fix(leads): keep a future follow-up as an activity on the lead

A scheduled follow-up only stored a date, and activities opened from a lead were not linked back, so they disappeared from the lead.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-03 16:19:32 +03:30
co-authored by Cursor
parent c26353624e
commit c337b0b4c7
6 changed files with 89 additions and 20 deletions
+20 -2
View File
@@ -36,6 +36,7 @@ import { GamificationService } from '../gamification/gamification.service';
import { tehranDayKey } from '../routines/routine-dates';
import { buildSearchWhere, ci } from '../common/search';
import { activityPriorWhere } from '../common/prior-archive';
import { leadsDeepWhere } from '../common/data-access';
import { ActivityStagesService, type BoardStageContext } from './activity-stages.service';
import {
ActivityTypesService,
@@ -45,7 +46,10 @@ import {
type DtoContext = { stages?: BoardStageContext; types?: SystemTypes };
type ActivityActor = Pick<Actor, 'userId' | 'role' | 'priorLens' | 'priorCutoff'>;
type ActivityActor = Pick<
Actor,
'userId' | 'role' | 'priorLens' | 'priorCutoff' | 'leadsAccess' | 'teamPeerIds'
>;
/** stageId = شناسهٔ مرحله یا 'all'؛ وقتی ست باشد فیلتر زمانی وضعیت را تحمیل نمی‌کند (جز عقب‌افتاده) */
type StageScope = { stageId: string };
@@ -324,7 +328,10 @@ export class ActivitiesService {
this.prisma.activity.findMany({
where,
include,
orderBy: [{ dueAt: 'asc' }, { createdAt: 'desc' }],
// روی پروندهٔ لید، تازه‌ترین (از جمله پیگیری آینده) اول بیاید تا از سقف صفحه نیفتد
orderBy: entity?.leadId
? [{ createdAt: 'desc' }]
: [{ dueAt: 'asc' }, { createdAt: 'desc' }],
skip: (page - 1) * take,
take,
}),
@@ -628,6 +635,7 @@ export class ActivitiesService {
personId: emptyToNull(dto.personId ?? null),
companyId: emptyToNull(dto.companyId ?? null),
dealId: emptyToNull(dto.dealId ?? null),
leadId: emptyToNull(dto.leadId ?? null),
assigneeId,
assignees: assigneeIds.length
? { connect: assigneeIds.map((id) => ({ id })) }
@@ -767,6 +775,7 @@ export class ActivitiesService {
if ('personId' in dto) data.personId = emptyToNull(dto.personId ?? null);
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
if ('dealId' in dto) data.dealId = emptyToNull(dto.dealId ?? null);
if ('leadId' in dto) data.leadId = emptyToNull(dto.leadId ?? null);
let assigneeConnect: Array<{ id: string }> | undefined;
if (dto.assigneeIds !== undefined || 'assigneeId' in dto) {
const current = await this.prisma.activity.findFirst({
@@ -1283,11 +1292,13 @@ export class ActivitiesService {
personId?: string | null;
companyId?: string | null;
dealId?: string | null;
leadId?: string | null;
},
) {
const personId = emptyToNull(dto.personId ?? null);
const companyId = emptyToNull(dto.companyId ?? null);
const dealId = emptyToNull(dto.dealId ?? null);
const leadId = emptyToNull(dto.leadId ?? null);
const scope = ownerWhere(actor);
if (personId) {
@@ -1311,6 +1322,13 @@ export class ActivitiesService {
});
if (!deal) throw new BadRequestException('معامله انتخاب‌شده معتبر نیست');
}
if (leadId) {
const lead = await this.prisma.lead.findFirst({
where: { id: leadId, organizationId, ...leadsDeepWhere(actor) },
select: { id: true },
});
if (!lead) throw new BadRequestException('لید انتخاب‌شده معتبر نیست');
}
}
private async notifyParticipants(
+10
View File
@@ -364,6 +364,11 @@ export class CreateActivityDto {
@IsString()
dealId?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
leadId?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
@@ -459,6 +464,11 @@ export class UpdateActivityDto {
@IsString()
dealId?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
leadId?: string | null;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()