Unify the web UI on shared controls and let pipeline columns be managed on the board.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 01:22:24 +03:30
co-authored by Cursor
parent f1aba1b386
commit b8ad4037b6
40 changed files with 2334 additions and 788 deletions
+137 -1
View File
@@ -1,4 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import type {
DealDto,
PipelineBoardDto,
@@ -114,6 +118,121 @@ export class PipelinesService {
return this.toPipelineDto(created);
}
async createStage(
organizationId: string,
pipelineId: string,
name: string,
): Promise<PipelineDto> {
const pipeline = await this.getOwnedPipeline(organizationId, pipelineId);
const trimmed = name.trim();
if (!trimmed) throw new BadRequestException('نام ستون لازم است');
const firstClosed = pipeline.stages.findIndex((s) => s.isWon || s.isLost);
const insertAt = firstClosed === -1 ? pipeline.stages.length : firstClosed;
await this.prisma.$transaction(async (tx) => {
await tx.pipelineStage.updateMany({
where: { pipelineId: pipeline.id, position: { gte: insertAt } },
data: { position: { increment: 1 } },
});
await tx.pipelineStage.create({
data: {
pipelineId: pipeline.id,
name: trimmed,
position: insertAt,
isWon: false,
isLost: false,
},
});
});
return this.getOwnedPipelineDto(organizationId, pipeline.id);
}
async updateStage(
organizationId: string,
pipelineId: string,
stageId: string,
name?: string,
): Promise<PipelineDto> {
const pipeline = await this.getOwnedPipeline(organizationId, pipelineId);
const stage = pipeline.stages.find((s) => s.id === stageId);
if (!stage) throw new NotFoundException('ستون پیدا نشد');
const trimmed = name?.trim() ?? '';
if (!trimmed) throw new BadRequestException('نام ستون لازم است');
await this.prisma.pipelineStage.update({
where: { id: stage.id },
data: { name: trimmed },
});
return this.getOwnedPipelineDto(organizationId, pipeline.id);
}
async deleteStage(
organizationId: string,
pipelineId: string,
stageId: string,
): Promise<PipelineDto> {
const pipeline = await this.getOwnedPipeline(organizationId, pipelineId);
const stage = pipeline.stages.find((s) => s.id === stageId);
if (!stage) throw new NotFoundException('ستون پیدا نشد');
if (pipeline.stages.length <= 1) {
throw new BadRequestException('حداقل یک ستون باید باقی بماند');
}
const dealCount = await this.prisma.deal.count({
where: { organizationId, stageId: stage.id },
});
if (dealCount > 0) {
throw new BadRequestException(
'ابتدا معاملات این ستون را به ستون دیگری منتقل کنید',
);
}
const remaining = pipeline.stages.filter((s) => s.id !== stage.id);
await this.prisma.$transaction([
this.prisma.pipelineStage.delete({ where: { id: stage.id } }),
...remaining.map((s, index) =>
this.prisma.pipelineStage.update({
where: { id: s.id },
data: { position: index },
}),
),
]);
return this.getOwnedPipelineDto(organizationId, pipeline.id);
}
async reorderStages(
organizationId: string,
pipelineId: string,
stageIds: string[],
): Promise<PipelineDto> {
const pipeline = await this.getOwnedPipeline(organizationId, pipelineId);
const currentIds = pipeline.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.pipelineStage.update({
where: { id },
data: { position: index },
}),
),
);
return this.getOwnedPipelineDto(organizationId, pipeline.id);
}
async getBoard(
organizationId: string,
pipelineId?: string,
@@ -232,6 +351,23 @@ export class PipelinesService {
};
}
private async getOwnedPipeline(organizationId: string, pipelineId: string) {
const pipeline = await this.prisma.pipeline.findFirst({
where: { id: pipelineId, organizationId },
include: { stages: { orderBy: { position: 'asc' } } },
});
if (!pipeline) throw new NotFoundException('کاریز پیدا نشد');
return pipeline;
}
private async getOwnedPipelineDto(
organizationId: string,
pipelineId: string,
): Promise<PipelineDto> {
const pipeline = await this.getOwnedPipeline(organizationId, pipelineId);
return this.toPipelineDto(pipeline);
}
private toPipelineDto(row: {
id: string;
name: string;