feat(leads): edit lead-board columns from the board
New-lead counts follow the first open column instead of its name, and cards no longer show an initials mark. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import type { LeadStageDto } from '@mento/shared';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
const DEFAULT_STAGES: Array<{
|
||||
@@ -74,20 +75,24 @@ export class LeadStagesService {
|
||||
where: { organizationId },
|
||||
_max: { position: true },
|
||||
});
|
||||
const row = await this.prisma.leadStage.create({
|
||||
data: {
|
||||
organizationId,
|
||||
name: dto.name.trim(),
|
||||
color: dto.color?.trim() || 'teal',
|
||||
showOnBoard: dto.showOnBoard !== false,
|
||||
isConverted: Boolean(dto.isConverted),
|
||||
isLost: Boolean(dto.isLost),
|
||||
isUnqualified: Boolean(dto.isUnqualified),
|
||||
position: (max._max.position ?? -1) + 1,
|
||||
},
|
||||
include: { _count: { select: { leads: true } } },
|
||||
});
|
||||
return this.toDto(row);
|
||||
try {
|
||||
const row = await this.prisma.leadStage.create({
|
||||
data: {
|
||||
organizationId,
|
||||
name: this.cleanName(dto.name),
|
||||
color: dto.color?.trim() || 'teal',
|
||||
showOnBoard: dto.showOnBoard !== false,
|
||||
isConverted: Boolean(dto.isConverted),
|
||||
isLost: Boolean(dto.isLost),
|
||||
isUnqualified: Boolean(dto.isUnqualified),
|
||||
position: (max._max.position ?? -1) + 1,
|
||||
},
|
||||
include: { _count: { select: { leads: true } } },
|
||||
});
|
||||
return this.toDto(row);
|
||||
} catch (e) {
|
||||
this.rethrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
@@ -103,10 +108,11 @@ export class LeadStagesService {
|
||||
position?: number;
|
||||
},
|
||||
): Promise<LeadStageDto> {
|
||||
try {
|
||||
await this.prisma.leadStage.updateMany({
|
||||
where: { id, organizationId },
|
||||
data: {
|
||||
...(dto.name != null ? { name: dto.name.trim() } : {}),
|
||||
...(dto.name != null ? { name: this.cleanName(dto.name) } : {}),
|
||||
...(dto.color != null ? { color: dto.color } : {}),
|
||||
...(dto.showOnBoard != null ? { showOnBoard: dto.showOnBoard } : {}),
|
||||
...(dto.isConverted != null ? { isConverted: dto.isConverted } : {}),
|
||||
@@ -122,6 +128,9 @@ export class LeadStagesService {
|
||||
include: { _count: { select: { leads: true } } },
|
||||
});
|
||||
return this.toDto(row);
|
||||
} catch (e) {
|
||||
this.rethrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
async reorder(organizationId: string, stageIds: string[]): Promise<LeadStageDto[]> {
|
||||
@@ -143,11 +152,27 @@ export class LeadStagesService {
|
||||
});
|
||||
if (!stage) return;
|
||||
if (stage._count.leads > 0) {
|
||||
throw new Error('مرحلهای که لید دارد قابل حذف نیست');
|
||||
throw new BadRequestException(
|
||||
'این ستون هنوز لید دارد و حذف نمیشود. لیدهای فعال و بایگانی را اول به ستون دیگری ببرید.',
|
||||
);
|
||||
}
|
||||
await this.prisma.leadStage.delete({ where: { id } });
|
||||
}
|
||||
|
||||
private cleanName(raw: string): string {
|
||||
const name = raw?.trim() ?? '';
|
||||
if (!name) throw new BadRequestException('نام ستون لازم است');
|
||||
if (name.length > 80) throw new BadRequestException('نام ستون حداکثر ۸۰ کاراکتر است');
|
||||
return name;
|
||||
}
|
||||
|
||||
private rethrow(e: unknown): never {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new BadRequestException('ستونی با این نام وجود دارد');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -107,6 +107,7 @@ export class LeadsController {
|
||||
await this.stages.remove(user.organizationId, id);
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
if (e instanceof BadRequestException) throw e;
|
||||
throw new BadRequestException(
|
||||
e instanceof Error ? e.message : 'حذف مرحله ناموفق بود',
|
||||
);
|
||||
|
||||
@@ -979,8 +979,10 @@ export class LeadsService {
|
||||
const stages = await this.stages.list(actor.organizationId);
|
||||
const convertedIds = stages.filter((s) => s.isConverted).map((s) => s.id);
|
||||
const lostIds = stages.filter((s) => s.isLost).map((s) => s.id);
|
||||
const interested = stages.find((s) => s.name.includes('علاقهمند'));
|
||||
const newStage = stages.find((s) => s.name === 'جدید');
|
||||
// همان مقصد فرم و ورود از اکسل: اولین ستون باز، مستقل از اسم «جدید»
|
||||
const newStage = stages.find(
|
||||
(s) => !s.isConverted && !s.isLost && !s.isUnqualified,
|
||||
);
|
||||
|
||||
const [
|
||||
total,
|
||||
@@ -988,7 +990,6 @@ export class LeadsService {
|
||||
unassigned,
|
||||
converted,
|
||||
lost,
|
||||
interestedCount,
|
||||
withFollowUp,
|
||||
callsLogged,
|
||||
byAssigneeRaw,
|
||||
@@ -1008,11 +1009,6 @@ export class LeadsService {
|
||||
where: { ...base, stageId: { in: lostIds } },
|
||||
})
|
||||
: Promise.resolve(0),
|
||||
interested
|
||||
? this.prisma.lead.count({
|
||||
where: { ...base, stageId: interested.id },
|
||||
})
|
||||
: Promise.resolve(0),
|
||||
this.prisma.lead.count({
|
||||
where: { ...base, nextFollowUpAt: { not: null } },
|
||||
}),
|
||||
@@ -1055,7 +1051,7 @@ export class LeadsService {
|
||||
.sort((a, b) => b.count - a.count),
|
||||
callsLogged,
|
||||
withFollowUp,
|
||||
interested: interestedCount,
|
||||
interested: 0,
|
||||
converted,
|
||||
lost,
|
||||
conversionRate: Math.round((converted / denom) * 1000) / 10,
|
||||
|
||||
Reference in New Issue
Block a user