Commit 22a7a58a authored by Administrator's avatar Administrator

Update 19 files via Son of Anton

parent fbb29954
......@@ -19,6 +19,8 @@ import { UsersModule } from './modules/users/users.module';
import { OnboardingModule } from './modules/onboarding/onboarding.module';
import { BoardsModule } from './modules/boards/boards.module';
import { ColumnsModule } from './modules/columns/columns.module';
import { LabelsModule } from './modules/labels/labels.module';
import { CardsModule } from './modules/cards/cards.module';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
import { RolesGuard } from './common/guards/roles.guard';
......@@ -44,6 +46,8 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
OnboardingModule,
BoardsModule,
ColumnsModule,
LabelsModule,
CardsModule,
],
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
......
import {
Injectable,
NotFoundException,
ForbiddenException,
Logger,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { RequestUser } from '../../common/decorators/current-user.decorator';
@Injectable()
export class CardAssignmentService {
private readonly logger = new Logger(CardAssignmentService.name);
constructor(private readonly prisma: PrismaService) {}
async assign(cardId: string, assigneeIds: string[], currentUser: RequestUser): Promise<any> {
const card = await this.prisma.card.findFirst({
where: { id: cardId, deletedAt: null },
include: {
column: { select: { boardId: true } },
assignees: { select: { id: true } },
},
});
if (!card) {
throw new NotFoundException('Card not found');
}
// Contractors cannot assign anyone
if (currentUser.role === 'CONTRACTOR') {
throw new ForbiddenException('Contractors cannot assign cards');
}
// TEAM_LEAD: can only assign members of their boards
if (currentUser.role === 'TEAM_LEAD') {
const membership = await this.prisma.boardMember.findUnique({
where: { boardId_userId: { boardId: card.column.boardId, userId: currentUser.id } },
});
if (!membership) {
throw new ForbiddenException('You can only assign cards on boards you manage');
}
}
// Verify all assignees are board members
for (const userId of assigneeIds) {
const user = await this.prisma.user.findFirst({
where: { id: userId, deletedAt: null },
});
if (!user) {
this.logger.warn(`User ${userId} not found, skipping assignment`);
continue;
}
const isMember = await this.prisma.boardMember.findUnique({
where: { boardId_userId: { boardId: card.column.boardId, userId } },
});
if (!isMember) {
this.logger.warn(`User ${userId} is not a member of board ${card.column.boardId}, skipping`);
continue;
}
// Connect the assignee
await this.prisma.card.update({
where: { id: cardId },
data: { assignees: { connect: { id: userId } } },
});
// Log activity
try {
await this.prisma.cardActivity.create({
data: {
cardId,
userId: currentUser.id,
action: 'ASSIGNED',
metadata: {
assigneeId: userId,
assigneeName: `${user.firstName} ${user.lastName}`,
},
},
});
} catch (err) {
this.logger.warn(`Failed to log assignment activity: ${err.message}`);
}
}
this.logger.log(`Card ${cardId}: assigned ${assigneeIds.length} user(s) by ${currentUser.email}`);
return this.getCardAssignees(cardId);
}
async unassign(cardId: string, userIds: string[], currentUser: RequestUser): Promise<any> {
const card = await this.prisma.card.findFirst({
where: { id: cardId, deletedAt: null },
});
if (!card) {
throw new NotFoundException('Card not found');
}
if (currentUser.role === 'CONTRACTOR') {
throw new ForbiddenException('Contractors cannot unassign cards');
}
for (const userId of userIds) {
await this.prisma.card.update({
where: { id: cardId },
data: { assignees: { disconnect: { id: userId } } },
});
try {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { firstName: true, lastName: true },
});
await this.prisma.cardActivity.create({
data: {
cardId,
userId: currentUser.id,
action: 'UNASSIGNED',
metadata: {
unassignedUserId: userId,
unassignedUserName: user ? `${user.firstName} ${user.lastName}` : userId,
},
},
});
} catch (err) {
this.logger.warn(`Failed to log unassignment activity: ${err.message}`);
}
}
this.logger.log(`Card ${cardId}: unassigned ${userIds.length} user(s) by ${currentUser.email}`);
return this.getCardAssignees(cardId);
}
async getCardAssignees(cardId: string): Promise<any[]> {
const card = await this.prisma.card.findUnique({
where: { id: cardId },
select: {
assignees: {
select: {
id: true,
firstName: true,
lastName: true,
displayName: true,
avatar: true,
role: true,
},
},
},
});
return card?.assignees || [];
}
}
\ No newline at end of file
import {
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
ConflictException,
Logger,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { MoveCardDto } from './dto/move-card.dto';
import { RequestUser } from '../../common/decorators/current-user.decorator';
@Injectable()
export class CardMovementService {
private readonly logger = new Logger(CardMovementService.name);
constructor(private readonly prisma: PrismaService) {}
async moveCard(cardId: string, dto: MoveCardDto, currentUser: RequestUser): Promise<any> {
const card = await this.prisma.card.findFirst({
where: { id: cardId, deletedAt: null },
include: {
column: { select: { id: true, boardId: true, type: true, name: true } },
assignees: { select: { id: true } },
},
});
if (!card) {
throw new NotFoundException('Card not found');
}
const targetColumn = await this.prisma.column.findUnique({
where: { id: dto.columnId },
});
if (!targetColumn) {
throw new NotFoundException('Target column not found');
}
if (targetColumn.boardId !== card.column.boardId) {
throw new BadRequestException('Cannot move card to a column on a different board');
}
const sourceColumn = card.column;
// If same column, just reorder
if (sourceColumn.id === targetColumn.id) {
return this.reorderInColumn(card, dto.position ?? 0);
}
// ─── PERMISSION ENFORCEMENT ─────────────────────────────────
this.enforceMovementPermissions(currentUser, sourceColumn, targetColumn, card);
// ─── FROZEN COLUMN LOGIC ─────────────────────────────────────
if (targetColumn.type === 'FROZEN') {
if (!dto.frozenReason || dto.frozenReason.length < 20) {
throw new BadRequestException('Moving to Frozen requires a reason of at least 20 characters');
}
}
// ─── WIP LIMIT CHECK ─────────────────────────────────────────
await this.checkWipLimits(targetColumn, card, currentUser);
// ─── CALCULATE POSITION ──────────────────────────────────────
const newPosition = await this.calculatePosition(targetColumn.id, dto.position);
// ─── TRACK TIMING ────────────────────────────────────────────
const now = new Date();
const updateData: any = {
columnId: targetColumn.id,
position: newPosition,
};
// Entering Frozen
if (targetColumn.type === 'FROZEN') {
updateData.frozenReason = dto.frozenReason;
updateData.frozenAt = now;
}
// Leaving Frozen — calculate frozen time
if (sourceColumn.type === 'FROZEN' && targetColumn.type !== 'FROZEN') {
updateData.frozenReason = null;
if (card.frozenAt) {
const frozenMs = now.getTime() - new Date(card.frozenAt).getTime();
const existingFrozenMs = (card.frozenTimeHours || 0) * 3600000;
updateData.frozenTimeHours = (existingFrozenMs + frozenMs) / 3600000;
}
updateData.frozenAt = null;
}
// First move to DOING — track cycle time start
if (targetColumn.type === 'DOING' && !card.startedAt) {
updateData.startedAt = now;
}
// Moving to DONE
if (targetColumn.type === 'DONE') {
updateData.completedAt = now;
// Calculate lead time (creation → done)
const leadMs = now.getTime() - new Date(card.createdAt).getTime();
updateData.leadTimeHours = leadMs / 3600000;
// Calculate cycle time (first doing → done, minus frozen time)
if (card.startedAt) {
const cycleMs = now.getTime() - new Date(card.startedAt).getTime();
const frozenMs = (updateData.frozenTimeHours || card.frozenTimeHours || 0) * 3600000;
updateData.cycleTimeHours = (cycleMs - frozenMs) / 3600000;
}
}
// Moving OUT of Done (reopening)
if (sourceColumn.type === 'DONE' && targetColumn.type !== 'DONE') {
updateData.completedAt = null;
updateData.leadTimeHours = null;
updateData.cycleTimeHours = null;
}
const updated = await this.prisma.card.update({
where: { id: cardId },
data: updateData,
});
// ─── LOG ACTIVITY ────────────────────────────────────────────
try {
await this.prisma.cardActivity.create({
data: {
cardId,
userId: currentUser.id,
action: 'MOVED',
metadata: {
fromColumn: sourceColumn.name,
fromColumnId: sourceColumn.id,
toColumn: targetColumn.name,
toColumnId: targetColumn.id,
frozenReason: dto.frozenReason || null,
},
},
});
} catch (err) {
this.logger.warn(`Failed to log card activity: ${err.message}`);
}
this.logger.log(
`Card ${cardId} moved from "${sourceColumn.name}" to "${targetColumn.name}" by ${currentUser.email}`,
);
return updated;
}
private enforceMovementPermissions(
currentUser: RequestUser,
sourceColumn: any,
targetColumn: any,
card: any,
): void {
const isSA = currentUser.role === 'SUPER_ADMIN';
const isAdmin = currentUser.role === 'ADMIN';
const isTL = currentUser.role === 'TEAM_LEAD';
const isContractor = currentUser.role === 'CONTRACTOR';
// Moving TO Done — only SA, Admin, TL
if (targetColumn.type === 'DONE') {
if (isContractor) {
throw new ForbiddenException(
'Only Project Leaders and Admins can mark tasks as Done',
);
}
}
// Moving FROM Done — only SA
if (sourceColumn.type === 'DONE') {
if (!isSA) {
throw new ForbiddenException('Only Super Admin can reopen cards from Done');
}
}
// Contractors can only move their own assigned cards (except to Done)
if (isContractor) {
const isAssigned = card.assignees?.some((a: any) => a.id === currentUser.id);
if (!isAssigned) {
throw new ForbiddenException('You can only move cards assigned to you');
}
}
}
private async checkWipLimits(targetColumn: any, card: any, currentUser: RequestUser): Promise<void> {
// Per-user WIP limit
if (targetColumn.wipLimit && targetColumn.wipLimit > 0) {
const assigneeIds = card.assignees?.map((a: any) => a.id) || [];
if (assigneeIds.length > 0) {
for (const assigneeId of assigneeIds) {
const userCardCount = await this.prisma.card.count({
where: {
columnId: targetColumn.id,
deletedAt: null,
assignees: { some: { id: assigneeId } },
},
});
if (userCardCount >= targetColumn.wipLimit) {
throw new BadRequestException(
`WIP limit reached in "${targetColumn.name}". Move a card out first.`,
);
}
}
}
}
// Total WIP limit
if (targetColumn.wipLimitTotal && targetColumn.wipLimitTotal > 0) {
const totalCount = await this.prisma.card.count({
where: { columnId: targetColumn.id, deletedAt: null },
});
if (totalCount >= targetColumn.wipLimitTotal) {
throw new BadRequestException(
`Total WIP limit reached in "${targetColumn.name}" (${totalCount}/${targetColumn.wipLimitTotal}). Move a card out first.`,
);
}
}
}
private async calculatePosition(columnId: string, requestedPosition?: number): Promise<number> {
const maxResult = await this.prisma.card.aggregate({
where: { columnId, deletedAt: null },
_max: { position: true },
});
const maxPosition = maxResult._max?.position || 0;
if (requestedPosition !== undefined && requestedPosition !== null) {
return requestedPosition;
}
// Place at the end
return maxPosition + 1;
}
private async reorderInColumn(card: any, newPosition: number): Promise<any> {
return this.prisma.card.update({
where: { id: card.id },
data: { position: newPosition },
});
}
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { CardsService } from './cards.service';
import { CardMovementService } from './card-movement.service';
import { CardAssignmentService } from './card-assignment.service';
import { CreateCardDto } from './dto/create-card.dto';
import { UpdateCardDto } from './dto/update-card.dto';
import { MoveCardDto } from './dto/move-card.dto';
import { CardFilterDto } from './dto/card-filter.dto';
import { DuplicateCardDto } from './dto/duplicate-card.dto';
import { AssignCardDto, UnassignCardDto, SetBountyDto } from './dto/assign-card.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('cards')
export class CardsController {
constructor(
private readonly cardsService: CardsService,
private readonly cardMovementService: CardMovementService,
private readonly cardAssignmentService: CardAssignmentService,
) {}
@Post()
async create(@Body() dto: CreateCardDto, @CurrentUser() user: RequestUser) {
return this.cardsService.create(dto, user);
}
@Get()
async findAll(@Query() filter: CardFilterDto, @CurrentUser() user: RequestUser) {
return this.cardsService.findAll(filter, user);
}
@Get(':id')
async findById(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.cardsService.findById(id, user);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateCardDto,
@CurrentUser() user: RequestUser,
) {
return this.cardsService.update(id, dto, user);
}
@Put(':id/move')
@HttpCode(HttpStatus.OK)
async moveCard(
@Param('id') id: string,
@Body() dto: MoveCardDto,
@CurrentUser() user: RequestUser,
) {
await this.cardMovementService.moveCard(id, dto, user);
return this.cardsService.findById(id, user);
}
@Put(':id/assign')
@HttpCode(HttpStatus.OK)
async assignCard(
@Param('id') id: string,
@Body() dto: AssignCardDto,
@CurrentUser() user: RequestUser,
) {
return this.cardAssignmentService.assign(id, dto.assigneeIds, user);
}
@Put(':id/unassign')
@HttpCode(HttpStatus.OK)
async unassignCard(
@Param('id') id: string,
@Body() dto: UnassignCardDto,
@CurrentUser() user: RequestUser,
) {
return this.cardAssignmentService.unassign(id, dto.userIds, user);
}
@Put(':id/bounty')
@HttpCode(HttpStatus.OK)
async setBounty(
@Param('id') id: string,
@Body() dto: SetBountyDto,
@CurrentUser() user: RequestUser,
) {
return this.cardsService.setBounty(id, dto.bountyPiasters, dto.bountySplitJson, user);
}
@Post(':id/duplicate')
async duplicateCard(
@Param('id') id: string,
@Body() dto: DuplicateCardDto,
@CurrentUser() user: RequestUser,
) {
return this.cardsService.duplicate(id, dto, user);
}
@Post(':id/archive')
@HttpCode(HttpStatus.OK)
async archiveCard(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.cardsService.archive(id, user);
return { message: 'Card archived' };
}
@Post(':id/restore')
@HttpCode(HttpStatus.OK)
async restoreCard(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.cardsService.restore(id, user);
}
@Delete(':id')
@Roles('SUPER_ADMIN')
@HttpCode(HttpStatus.OK)
async permanentDelete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.cardsService.permanentDelete(id, user);
return { message: 'Card permanently deleted' };
}
@Get(':id/activity')
async getActivity(@Param('id') id: string, @CurrentUser() user: RequestUser) {
return this.cardsService.getCardActivity(id, user);
}
@Post(':id/watch')
@HttpCode(HttpStatus.OK)
async watchCard(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.cardsService.watchCard(id, user);
return { message: 'Now watching this card' };
}
@Delete(':id/watch')
@HttpCode(HttpStatus.OK)
async unwatchCard(@Param('id') id: string, @CurrentUser() user: RequestUser) {
await this.cardsService.unwatchCard(id, user);
return { message: 'Stopped watching this card' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { CardsController } from './cards.controller';
import { CardsService } from './cards.service';
import { CardMovementService } from './card-movement.service';
import { CardAssignmentService } from './card-assignment.service';
@Module({
controllers: [CardsController],
providers: [CardsService, CardMovementService, CardAssignmentService],
exports: [CardsService, CardMovementService, CardAssignmentService],
})
export class CardsModule {}
\ No newline at end of file
This diff is collapsed.
import { IsString, IsArray, IsOptional } from 'class-validator';
export class AssignCardDto {
@IsArray()
@IsString({ each: true })
assigneeIds: string[];
}
export class UnassignCardDto {
@IsArray()
@IsString({ each: true })
userIds: string[];
}
export class SetBountyDto {
@IsOptional()
bountyPiasters: number;
@IsOptional()
@IsString()
bountySplitJson?: string;
// JSON string of { userId: percentage } for multi-assignee split
}
\ No newline at end of file
import { IsOptional, IsString, IsBoolean, IsArray, IsEnum } from 'class-validator';
import { Type } from 'class-transformer';
import { PaginationDto } from '../../../common/dto/pagination.dto';
export class CardFilterDto extends PaginationDto {
@IsOptional()
@IsString()
boardId?: string;
@IsOptional()
@IsString()
columnId?: string;
@IsOptional()
@IsString()
assigneeId?: string;
@IsOptional()
@IsString()
priority?: string;
@IsOptional()
@IsString({ each: true })
labelIds?: string;
// Comma-separated label IDs
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
hasBounty?: boolean;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isOverdue?: boolean;
@IsOptional()
@IsString()
dueDateFrom?: string;
@IsOptional()
@IsString()
dueDateTo?: string;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isArchived?: boolean;
@IsOptional()
@IsString()
createdById?: string;
}
\ No newline at end of file
export class CardSummaryResponseDto {
id: string;
cardNumber: string;
title: string;
boardId: string;
columnId: string;
position: number;
priority: string;
dueDate: string | null;
isOverdue: boolean;
bountyPiasters: number;
coverImage: string | null;
isArchived: boolean;
assigneeCount: number;
commentCount: number;
attachmentCount: number;
checklistProgress: { completed: number; total: number } | null;
labels: Array<{ id: string; name: string; color: string; textColor: string | null }>;
assignees: Array<{ id: string; firstName: string; lastName: string; avatar: string | null }>;
createdAt: string;
}
export class CardDetailResponseDto extends CardSummaryResponseDto {
description: string | null;
estimatedHours: number | null;
actualHours: number | null;
frozenReason: string | null;
bountySplit: any;
version: number;
startedAt: string | null;
completedAt: string | null;
leadTimeHours: number | null;
cycleTimeHours: number | null;
frozenTimeHours: number | null;
createdBy: { id: string; firstName: string; lastName: string; avatar: string | null };
watchers: Array<{ id: string; firstName: string; lastName: string; avatar: string | null }>;
columnName: string;
boardName: string;
boardKey: string;
updatedAt: string;
}
\ No newline at end of file
import {
IsString,
IsOptional,
IsArray,
IsNumber,
IsInt,
IsBoolean,
Min,
Max,
MinLength,
MaxLength,
} from 'class-validator';
export class CreateCardDto {
@IsString()
boardId: string;
@IsOptional()
@IsString()
columnId?: string;
// If not provided, defaults to Backlog column of the board
@IsString()
@MinLength(1)
@MaxLength(200)
title: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
priority?: string;
@IsOptional()
@IsString()
dueDate?: string;
@IsOptional()
@IsNumber()
@Min(0)
estimatedHours?: number;
@IsOptional()
@IsArray()
@IsString({ each: true })
assigneeIds?: string[];
@IsOptional()
@IsArray()
@IsString({ each: true })
labelIds?: string[];
@IsOptional()
@IsInt()
@Min(0)
bountyPiasters?: number;
}
\ No newline at end of file
import { IsString, IsOptional, IsBoolean } from 'class-validator';
export class DuplicateCardDto {
@IsOptional()
@IsString()
targetBoardId?: string;
// If not provided, duplicates to the same board
@IsOptional()
@IsBoolean()
keepDeadline?: boolean;
}
\ No newline at end of file
import { IsString, IsOptional, IsNumber, MinLength } from 'class-validator';
export class MoveCardDto {
@IsString()
columnId: string;
@IsOptional()
@IsNumber()
position?: number;
@IsOptional()
@IsString()
@MinLength(20, { message: 'Frozen reason must be at least 20 characters' })
frozenReason?: string;
// Required when moving to Frozen column
}
\ No newline at end of file
import {
IsString,
IsOptional,
IsNumber,
IsInt,
Min,
Max,
MinLength,
MaxLength,
IsArray,
} from 'class-validator';
export class UpdateCardDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(200)
title?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
priority?: string;
@IsOptional()
@IsString()
dueDate?: string;
@IsOptional()
@IsNumber()
@Min(0)
estimatedHours?: number;
@IsOptional()
@IsNumber()
@Min(0)
actualHours?: number;
@IsOptional()
@IsString()
coverImage?: string;
@IsOptional()
@IsInt()
@Min(0)
version?: number;
// For optimistic locking
}
\ No newline at end of file
import { IsString, IsOptional, MinLength, MaxLength, Matches } from 'class-validator';
export class CreateLabelDto {
@IsString()
@MinLength(1)
@MaxLength(20)
name: string;
@IsString()
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'Color must be a valid hex color (e.g., #EF4444)' })
color: string;
@IsOptional()
@IsString()
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'Text color must be a valid hex color' })
textColor?: string;
@IsOptional()
@IsString()
boardId?: string;
// If boardId is provided → BOARD scope. If not → ORGANIZATION scope.
}
\ No newline at end of file
export class LabelResponseDto {
id: string;
name: string;
color: string;
textColor: string | null;
scope: string;
boardId: string | null;
createdById: string | null;
cardCount?: number;
createdAt: string;
updatedAt: string;
}
\ No newline at end of file
import { IsString, IsOptional, MinLength, MaxLength, Matches } from 'class-validator';
export class UpdateLabelDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(20)
name?: string;
@IsOptional()
@IsString()
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'Color must be a valid hex color' })
color?: string;
@IsOptional()
@IsString()
@Matches(/^#[0-9A-Fa-f]{6}$/, { message: 'Text color must be a valid hex color' })
textColor?: string;
}
\ No newline at end of file
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { LabelsService } from './labels.service';
import { CreateLabelDto } from './dto/create-label.dto';
import { UpdateLabelDto } from './dto/update-label.dto';
import { CurrentUser, RequestUser } from '../../common/decorators/current-user.decorator';
import { Roles } from '../../common/decorators/roles.decorator';
@Controller('labels')
export class LabelsController {
constructor(private readonly labelsService: LabelsService) {}
@Post()
async create(@Body() dto: CreateLabelDto, @CurrentUser() user: RequestUser) {
return this.labelsService.create(dto, user);
}
@Get('organization')
async findOrgLabels() {
return this.labelsService.findOrgLabels();
}
@Get('board/:boardId')
async findBoardLabels(@Param('boardId') boardId: string, @CurrentUser() user: RequestUser) {
return this.labelsService.findBoardLabels(boardId, user);
}
@Get(':id')
async findById(@Param('id') id: string) {
return this.labelsService.findById(id);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateLabelDto,
@CurrentUser() user: RequestUser,
) {
return this.labelsService.update(id, dto, user);
}
@Delete(':id')
@HttpCode(HttpStatus.OK)
async delete(@Param('id') id: string, @CurrentUser() user: RequestUser) {
const result = await this.labelsService.delete(id, user);
return { message: `Label deleted. ${result.affectedCards} cards affected.`, ...result };
}
@Post(':labelId/cards/:cardId')
@HttpCode(HttpStatus.OK)
async applyToCard(
@Param('labelId') labelId: string,
@Param('cardId') cardId: string,
@CurrentUser() user: RequestUser,
) {
await this.labelsService.applyToCard(labelId, cardId, user);
return { message: 'Label applied to card' };
}
@Delete(':labelId/cards/:cardId')
@HttpCode(HttpStatus.OK)
async removeFromCard(
@Param('labelId') labelId: string,
@Param('cardId') cardId: string,
@CurrentUser() user: RequestUser,
) {
await this.labelsService.removeFromCard(labelId, cardId, user);
return { message: 'Label removed from card' };
}
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { LabelsController } from './labels.controller';
import { LabelsService } from './labels.service';
@Module({
controllers: [LabelsController],
providers: [LabelsService],
exports: [LabelsService],
})
export class LabelsModule {}
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment