Editing of the project's Kariz page

This commit is contained in:
2026-09-15 19:13:30 +03:30
parent 44f9849d0e
commit e61fad95c6
25 changed files with 1751 additions and 567 deletions
+22
View File
@@ -1,5 +1,6 @@
import { Type } from 'class-transformer';
import {
IsArray,
IsNumber,
IsOptional,
IsString,
@@ -16,6 +17,27 @@ export class CreateProjectBoardDto {
name!: string;
}
export class CreateProjectBoardStageDto {
@IsString()
@MinLength(1)
@MaxLength(80)
name!: string;
}
export class UpdateProjectBoardStageDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(80)
name?: string;
}
export class ReorderProjectBoardStagesDto {
@IsArray()
@IsString({ each: true })
stageIds!: string[];
}
export class CreateProjectCardDto {
@IsString()
@MinLength(2)
@@ -23,8 +23,11 @@ import { RolesGuard } from '../auth/roles.guard';
import type { Actor } from '../common/record-scope';
import {
CreateProjectBoardDto,
CreateProjectBoardStageDto,
CreateProjectCardDto,
MoveProjectCardDto,
ReorderProjectBoardStagesDto,
UpdateProjectBoardStageDto,
UpdateProjectCardDto,
} from './dto';
import { ProjectBoardsService } from './project-boards.service';
@@ -61,6 +64,56 @@ export class ProjectBoardsController {
return this.boards.create(user.organizationId, dto.name);
}
@Post('project-boards/:boardId/stages')
@Roles('owner', 'admin')
createStage(
@CurrentUser() user: Actor,
@Param('boardId') boardId: string,
@Body() dto: CreateProjectBoardStageDto,
) {
return this.boards.createStage(user.organizationId, boardId, dto.name);
}
@Patch('project-boards/:boardId/stages/order')
@Roles('owner', 'admin')
reorderStages(
@CurrentUser() user: Actor,
@Param('boardId') boardId: string,
@Body() dto: ReorderProjectBoardStagesDto,
) {
return this.boards.reorderStages(
user.organizationId,
boardId,
dto.stageIds,
);
}
@Patch('project-boards/:boardId/stages/:stageId')
@Roles('owner', 'admin')
updateStage(
@CurrentUser() user: Actor,
@Param('boardId') boardId: string,
@Param('stageId') stageId: string,
@Body() dto: UpdateProjectBoardStageDto,
) {
return this.boards.updateStage(
user.organizationId,
boardId,
stageId,
dto.name,
);
}
@Delete('project-boards/:boardId/stages/:stageId')
@Roles('owner', 'admin')
deleteStage(
@CurrentUser() user: Actor,
@Param('boardId') boardId: string,
@Param('stageId') stageId: string,
) {
return this.boards.deleteStage(user.organizationId, boardId, stageId);
}
@Get('project-cards/export')
async exportCards(
@CurrentUser() user: Actor,
@@ -299,6 +299,114 @@ export class ProjectBoardsService {
await this.prisma.projectCard.delete({ where: { id } });
}
async createStage(
organizationId: string,
boardId: string,
name: string,
): Promise<ProjectBoardDto> {
const board = await this.getOwnedBoard(organizationId, boardId);
const trimmed = name.trim();
if (!trimmed) throw new BadRequestException('نام ستون لازم است');
await this.prisma.projectBoardStage.create({
data: {
boardId: board.id,
name: trimmed,
position: board.stages.length,
},
});
return this.getOwnedBoardDto(organizationId, board.id);
}
async updateStage(
organizationId: string,
boardId: string,
stageId: string,
name?: string,
): Promise<ProjectBoardDto> {
const board = await this.getOwnedBoard(organizationId, boardId);
const stage = board.stages.find((s) => s.id === stageId);
if (!stage) throw new NotFoundException('ستون پیدا نشد');
const trimmed = name?.trim() ?? '';
if (!trimmed) throw new BadRequestException('نام ستون لازم است');
await this.prisma.projectBoardStage.update({
where: { id: stage.id },
data: { name: trimmed },
});
return this.getOwnedBoardDto(organizationId, board.id);
}
async deleteStage(
organizationId: string,
boardId: string,
stageId: string,
): Promise<ProjectBoardDto> {
const board = await this.getOwnedBoard(organizationId, boardId);
const stage = board.stages.find((s) => s.id === stageId);
if (!stage) throw new NotFoundException('ستون پیدا نشد');
if (board.stages.length <= 1) {
throw new BadRequestException('حداقل یک ستون باید باقی بماند');
}
const cardCount = await this.prisma.projectCard.count({
where: { organizationId, stageId: stage.id },
});
if (cardCount > 0) {
throw new BadRequestException(
'ابتدا کارت‌های این ستون را به ستون دیگری منتقل کنید',
);
}
const remaining = board.stages.filter((s) => s.id !== stage.id);
await this.prisma.$transaction([
this.prisma.projectBoardStage.delete({ where: { id: stage.id } }),
...remaining.map((s, index) =>
this.prisma.projectBoardStage.update({
where: { id: s.id },
data: { position: index },
}),
),
]);
return this.getOwnedBoardDto(organizationId, board.id);
}
async reorderStages(
organizationId: string,
boardId: string,
stageIds: string[],
): Promise<ProjectBoardDto> {
const board = await this.getOwnedBoard(organizationId, boardId);
const currentIds = board.stages.map((s) => s.id);
const unique = new Set(stageIds);
if (
stageIds.length !== currentIds.length ||
unique.size !== currentIds.length ||
currentIds.some((id) => !unique.has(id))
) {
throw new BadRequestException('ترتیب ستون‌ها نامعتبر است');
}
await this.prisma.$transaction(
stageIds.map((id, index) =>
this.prisma.projectBoardStage.update({
where: { id },
data: { position: index },
}),
),
);
return this.getOwnedBoardDto(organizationId, board.id);
}
private async getOwnedBoard(organizationId: string, boardId: string) {
const board = await this.prisma.projectBoard.findFirst({
where: { id: boardId, organizationId },
include: { stages: { orderBy: { position: 'asc' as const } } },
});
if (!board) throw new NotFoundException('برد پیدا نشد');
return board;
}
private async getOwnedBoardDto(organizationId: string, boardId: string) {
const board = await this.getOwnedBoard(organizationId, boardId);
return this.toBoardDto(board);
}
toCardDto(row: {
id: string;
title: string;
@@ -108,6 +108,7 @@ export class ProjectCardsExcelService {
async import(actor: Actor, file: Express.Multer.File): Promise<ImportContactsResult> {
const rows = await readMappedRows(file, CASE_ALIASES);
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
const affected = new Map<string, string>();
for (let i = 0; i < rows.length; i++) {
const row = rows[i]!;
@@ -122,12 +123,13 @@ export class ProjectCardsExcelService {
}
try {
const { boardId, stageId } = await this.resolveBoardAndStage(
const { boardId, stageId, boardName } = await this.resolveBoardAndStage(
actor.organizationId,
row.boardName,
row.stageName,
isClosedStatus(row.status),
);
affected.set(boardId, boardName);
const duplicate = await this.prisma.projectCard.findFirst({
where: { organizationId: actor.organizationId, boardId, title },
select: { id: true },
@@ -165,6 +167,12 @@ export class ProjectCardsExcelService {
}
}
const entries = [...affected.entries()];
if (entries[0]) {
result.boardId = entries[0][0];
result.boards = entries.map(([, n]) => n);
}
return result;
}
@@ -204,19 +212,19 @@ export class ProjectCardsExcelService {
const existing = board.stages.find(
(s) => s.name.trim().toLowerCase() === wanted.toLowerCase(),
);
if (existing) return { boardId: board.id, stageId: existing.id };
if (existing) return { boardId: board.id, stageId: existing.id, boardName: board.name };
const maxPos = board.stages.reduce((m, s) => Math.max(m, s.position), -1);
const stage = await this.prisma.projectBoardStage.create({
data: { boardId: board.id, name: wanted, position: maxPos + 1 },
});
return { boardId: board.id, stageId: stage.id };
return { boardId: board.id, stageId: stage.id, boardName: board.name };
}
const stage = closed
? board.stages[board.stages.length - 1]
: board.stages[0];
if (!stage) throw new Error('برد مرحله‌ای ندارد');
return { boardId: board.id, stageId: stage.id };
return { boardId: board.id, stageId: stage.id, boardName: board.name };
}
private async findPersonId(organizationId: string, row: Record<string, string>) {