fix(leads): give a new form lead the same owner as its first call
When a form has no round-robin assignees, the lead now follows the person the automation assigns to the first call. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,19 @@ export type TemplateVars = {
|
||||
formValues?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* مسئول سرنخ فقط از اولین فعالیتی که مسئول دارد پر میشود.
|
||||
* اگر قبلاً گرفته شده یا این فعالیت مسئول ندارد، null برمیگردد.
|
||||
*/
|
||||
export function claimLeadAssignee(
|
||||
alreadyClaimed: boolean,
|
||||
activityAssigneeId: string | null | undefined,
|
||||
): string | null {
|
||||
if (alreadyClaimed) return null;
|
||||
const id = activityAssigneeId?.trim();
|
||||
return id || null;
|
||||
}
|
||||
|
||||
/** counter یکواحد افزایشیافته از AutomationCursor است (از ۱ شروع میشود) */
|
||||
export function pickRoundRobin(memberIds: string[], counter: number): string | null {
|
||||
const ids = [...new Set(memberIds.filter(Boolean))].sort();
|
||||
|
||||
@@ -34,6 +34,7 @@ import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { LoyaltyService } from '../loyalty/loyalty.service';
|
||||
import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
|
||||
import {
|
||||
claimLeadAssignee,
|
||||
formatFormAnswers,
|
||||
pickRoundRobin,
|
||||
renderAutomationTemplate,
|
||||
@@ -59,6 +60,12 @@ export type AutomationContext = {
|
||||
formName?: string | null;
|
||||
formValues?: Record<string, unknown> | null;
|
||||
formFields?: FormFieldLabel[] | null;
|
||||
/**
|
||||
* فرم سرنخ مسئول نوبتی ندارد: مسئول سرنخ همان مسئول اولین فعالیت این اجرا میشود.
|
||||
* داخل همان اجرا یکبار ست میشود تا فعالیت بعدی مسئول را عوض نکند.
|
||||
*/
|
||||
syncLeadAssignee?: boolean;
|
||||
leadAssigneeSynced?: boolean;
|
||||
};
|
||||
|
||||
type AutomationFacts = {
|
||||
@@ -406,6 +413,54 @@ export class AutomationsService {
|
||||
entityId: activity.id,
|
||||
});
|
||||
}
|
||||
await this.syncLeadAssigneeFromActivity(ctx, type === 'call' ? assigneeId : null);
|
||||
}
|
||||
|
||||
/** سرنخ تازهای که فرم برایش مسئول نوبتی ندارد، مسئول اولین تماس را میگیرد. */
|
||||
private async syncLeadAssigneeFromActivity(
|
||||
ctx: AutomationContext,
|
||||
activityAssigneeId: string | null,
|
||||
) {
|
||||
const assigneeId = claimLeadAssignee(ctx.leadAssigneeSynced === true, activityAssigneeId);
|
||||
if (!ctx.syncLeadAssignee || !assigneeId || !ctx.leadId) return;
|
||||
|
||||
const existing = await this.prisma.lead.findFirst({
|
||||
where: {
|
||||
id: ctx.leadId,
|
||||
organizationId: ctx.organizationId,
|
||||
archivedAt: null,
|
||||
},
|
||||
select: { assigneeId: true },
|
||||
});
|
||||
if (!existing) return;
|
||||
|
||||
ctx.leadAssigneeSynced = true;
|
||||
ctx.assigneeId = assigneeId;
|
||||
if (existing.assigneeId === assigneeId) return;
|
||||
|
||||
await this.prisma.lead.updateMany({
|
||||
where: {
|
||||
id: ctx.leadId,
|
||||
organizationId: ctx.organizationId,
|
||||
archivedAt: null,
|
||||
},
|
||||
data: { assigneeId },
|
||||
});
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id: assigneeId },
|
||||
select: { fullName: true },
|
||||
});
|
||||
const name = user?.fullName?.trim() || 'کارشناس';
|
||||
await this.prisma.leadEvent.create({
|
||||
data: {
|
||||
organizationId: ctx.organizationId,
|
||||
leadId: ctx.leadId,
|
||||
type: 'assigned',
|
||||
title: `به «${name}» اختصاص داده شد`,
|
||||
body: 'مسئول سرنخ همان مسئول اولین تماس اتوماسیون شد.',
|
||||
meta: { assigneeId, source: 'automation' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveAssignee(
|
||||
|
||||
@@ -391,6 +391,7 @@ export class LeadFormsService {
|
||||
sourceId: form.id,
|
||||
sourceType: 'lead_form',
|
||||
leadId,
|
||||
syncLeadAssignee: !ownerId && leadCreated && !!leadId,
|
||||
mobile: mapped.mobile ?? mapped.phone ?? null,
|
||||
formName: form.name,
|
||||
formValues: values,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type AutomationStep,
|
||||
} from '@mento/shared';
|
||||
import {
|
||||
claimLeadAssignee,
|
||||
formatFormAnswers,
|
||||
pickRoundRobin,
|
||||
renderAutomationTemplate,
|
||||
@@ -136,6 +137,13 @@ describe('اتوماسیون شاخهای فرم', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('مسئول سرنخ فقط از اولین فعالیت دارای مسئول پر میشود', () => {
|
||||
assert.equal(claimLeadAssignee(false, null), null);
|
||||
assert.equal(claimLeadAssignee(false, ' '), null);
|
||||
assert.equal(claimLeadAssignee(false, 'romina'), 'romina');
|
||||
assert.equal(claimLeadAssignee(true, 'fatemeh'), null);
|
||||
});
|
||||
|
||||
it('تقسیم نوبتی مساوی و پایدار', () => {
|
||||
const team = ['u3', 'u1', 'u2'];
|
||||
const picks = [1, 2, 3, 4, 5, 6].map((n) => pickRoundRobin(team, n));
|
||||
|
||||
@@ -855,7 +855,7 @@ onMounted(load);
|
||||
{{ u.fullName }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!assigneeIds.length" class="settings-empty">مسئولی انتخاب نشده؛ لید بدون مسئول ثبت میشود.</p>
|
||||
<p v-if="!assigneeIds.length" class="settings-empty">مسئولی انتخاب نشده. سرنخ جدید مسئولش همان مسئول اولین تماس اتوماسیون میشود؛ اگر تماسی ساخته نشود، مالک سازمان مسئول میماند.</p>
|
||||
</article>
|
||||
|
||||
<article class="settings-card optional">
|
||||
|
||||
Reference in New Issue
Block a user