Let automations fire on more CRM events and open the studio from the main nav.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 21:43:17 +03:30
co-authored by Cursor
parent 51684daa61
commit f1aba1b386
20 changed files with 419 additions and 49 deletions
@@ -0,0 +1,19 @@
CREATE TYPE "AutomationTrigger_new" AS ENUM (
'person_created',
'lead_submitted',
'deal_created',
'deal_stage_changed',
'deal_won',
'deal_lost',
'ticket_created',
'ticket_resolved',
'invoice_paid'
);
ALTER TABLE "AutomationRule"
ALTER COLUMN "trigger" TYPE "AutomationTrigger_new"
USING ("trigger"::text::"AutomationTrigger_new");
ALTER TYPE "AutomationTrigger" RENAME TO "AutomationTrigger_old";
ALTER TYPE "AutomationTrigger_new" RENAME TO "AutomationTrigger";
DROP TYPE "AutomationTrigger_old";
@@ -0,0 +1,21 @@
CREATE TYPE "AutomationTrigger_new" AS ENUM (
'person_created',
'lead_submitted',
'deal_created',
'deal_stage_changed',
'deal_won',
'deal_lost',
'ticket_created',
'ticket_resolved',
'invoice_issued',
'invoice_paid',
'activity_done'
);
ALTER TABLE "AutomationRule"
ALTER COLUMN "trigger" TYPE "AutomationTrigger_new"
USING ("trigger"::text::"AutomationTrigger_new");
ALTER TYPE "AutomationTrigger" RENAME TO "AutomationTrigger_old";
ALTER TYPE "AutomationTrigger_new" RENAME TO "AutomationTrigger";
DROP TYPE "AutomationTrigger_old";
+9
View File
@@ -108,8 +108,17 @@ enum TicketPriority {
}
enum AutomationTrigger {
person_created
lead_submitted
deal_created
deal_stage_changed
deal_won
deal_lost
ticket_created
ticket_resolved
invoice_issued
invoice_paid
activity_done
}
enum AutomationAction {
+2 -1
View File
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { ActivitiesController } from './activities.controller';
import { ActivitiesService } from './activities.service';
import { NotificationsModule } from '../notifications/notifications.module';
import { AutomationsModule } from '../automations/automations.module';
@Module({
imports: [NotificationsModule],
imports: [NotificationsModule, AutomationsModule],
controllers: [ActivitiesController],
providers: [ActivitiesService],
exports: [ActivitiesService],
@@ -14,6 +14,7 @@ import type {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { AutomationsService } from '../automations/automations.service';
import { type Actor, assigneeWhere, ownerWhere } from '../common/record-scope';
type ActivityActor = Pick<Actor, 'userId' | 'role'>;
@@ -53,6 +54,7 @@ export class ActivitiesService {
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
private readonly automations: AutomationsService,
) {}
async summary(
@@ -245,11 +247,18 @@ export class ActivitiesService {
dto.status === 'done' ? new Date() : dto.status === 'open' ? null : undefined;
}
const existing = await this.prisma.activity.findFirst({
where: { id, organizationId },
select: { status: true },
});
const row = await this.prisma.activity.update({
where: { id },
data,
include,
});
if (dto.status === 'done' && existing?.status !== 'done') {
await this.fireDone(organizationId, row);
}
return this.toDto(row);
}
@@ -259,11 +268,18 @@ export class ActivitiesService {
actor: ActivityActor,
): Promise<ActivityDto> {
await this.ensureExists(organizationId, id, actor);
const existing = await this.prisma.activity.findFirst({
where: { id, organizationId },
select: { status: true },
});
const row = await this.prisma.activity.update({
where: { id },
data: { status: 'done', completedAt: new Date() },
include,
});
if (existing?.status !== 'done') {
await this.fireDone(organizationId, row);
}
return this.toDto(row);
}
@@ -350,6 +366,30 @@ export class ActivitiesService {
}
}
private async fireDone(
organizationId: string,
row: {
id: string;
title: string;
person: { id: string } | null;
company: { id: string } | null;
deal: { id: string } | null;
assignee: { id: string } | null;
},
) {
await this.automations.runTrigger('activity_done', {
organizationId,
entityType: 'activity',
entityId: row.id,
title: row.title,
personId: row.person?.id ?? null,
companyId: row.company?.id ?? null,
dealId: row.deal?.id ?? null,
assigneeId: row.assignee?.id ?? null,
ownerId: row.assignee?.id ?? null,
});
}
private async ensureExists(
organizationId: string,
id: string,
+69 -16
View File
@@ -32,7 +32,7 @@ import { SmsService } from '../common/sms.service';
export type AutomationContext = {
organizationId: string;
entityType: 'deal' | 'ticket';
entityType: 'deal' | 'ticket' | 'person' | 'invoice' | 'activity';
entityId: string;
title: string;
personId?: string | null;
@@ -182,15 +182,65 @@ export class AutomationsService {
ticketPriority: null,
};
}
const ticket = await this.prisma.ticket.findFirst({
where: { id: ctx.entityId, organizationId: ctx.organizationId },
select: { priority: true, personId: true, companyId: true },
});
if (ctx.entityType === 'ticket') {
const ticket = await this.prisma.ticket.findFirst({
where: { id: ctx.entityId, organizationId: ctx.organizationId },
select: { priority: true, personId: true, companyId: true },
});
return {
amount: null,
hasPerson: Boolean(ticket?.personId ?? ctx.personId),
hasCompany: Boolean(ticket?.companyId ?? ctx.companyId),
ticketPriority: ticket?.priority ?? null,
};
}
if (ctx.entityType === 'invoice') {
const invoice = await this.prisma.invoice.findFirst({
where: { id: ctx.entityId, organizationId: ctx.organizationId },
select: {
personId: true,
companyId: true,
discountAmount: true,
taxPercent: true,
lineItems: { select: { quantity: true, unitPrice: true } },
},
});
let amount: number | null = null;
if (invoice) {
const subtotal = invoice.lineItems.reduce(
(sum, item) => sum + Number(item.quantity) * Number(item.unitPrice),
0,
);
const discount = Number(invoice.discountAmount ?? 0);
const taxPercent = Number(invoice.taxPercent ?? 0);
amount = Math.abs(
subtotal - discount + Math.max(0, subtotal - discount) * (taxPercent / 100),
);
}
return {
amount,
hasPerson: Boolean(invoice?.personId ?? ctx.personId),
hasCompany: Boolean(invoice?.companyId ?? ctx.companyId),
ticketPriority: null,
};
}
if (ctx.entityType === 'activity') {
const activity = await this.prisma.activity.findFirst({
where: { id: ctx.entityId, organizationId: ctx.organizationId },
select: { personId: true, companyId: true, dealId: true },
});
return {
amount: null,
hasPerson: Boolean(activity?.personId ?? ctx.personId),
hasCompany: Boolean(activity?.companyId ?? ctx.companyId),
ticketPriority: null,
};
}
return {
amount: null,
hasPerson: Boolean(ticket?.personId ?? ctx.personId),
hasCompany: Boolean(ticket?.companyId ?? ctx.companyId),
ticketPriority: ticket?.priority ?? null,
hasPerson: Boolean(ctx.personId),
hasCompany: Boolean(ctx.companyId),
ticketPriority: null,
};
}
@@ -227,10 +277,7 @@ export class AutomationsService {
type: 'automation',
title: 'فعالیت خودکار',
body: title,
link:
ctx.entityType === 'deal'
? `/deals/${ctx.entityId}`
: `/tickets/${ctx.entityId}`,
link: entityLink(ctx),
entityType: ctx.entityType,
entityId: ctx.entityId,
});
@@ -263,10 +310,7 @@ export class AutomationsService {
const wantApp = channel === 'app' || channel === 'both';
const wantSms = channel === 'sms' || channel === 'both';
const smsText = (config.body?.trim() || title).slice(0, 160);
const link =
ctx.entityType === 'deal'
? `/deals/${ctx.entityId}`
: `/tickets/${ctx.entityId}`;
const link = entityLink(ctx);
if (config.notifyTarget === 'person') {
if (wantSms) {
@@ -413,3 +457,12 @@ function defaultTitle(action: AutomationAction, entityTitle: string) {
if (action === 'create_notification') return `اتوماسیون: ${entityTitle}`;
return entityTitle;
}
function entityLink(ctx: AutomationContext) {
if (ctx.entityType === 'deal') return `/deals/${ctx.entityId}`;
if (ctx.entityType === 'ticket') return `/tickets/${ctx.entityId}`;
if (ctx.entityType === 'invoice') return `/invoices/${ctx.entityId}`;
if (ctx.entityType === 'person') return `/contacts/people/${ctx.entityId}`;
if (ctx.entityType === 'activity') return `/activities/${ctx.entityId}`;
return '/';
}
+13 -1
View File
@@ -12,7 +12,19 @@ import {
ValidateIf,
} from 'class-validator';
const TRIGGERS = ['deal_won', 'ticket_resolved'] as const;
const TRIGGERS = [
'person_created',
'lead_submitted',
'deal_created',
'deal_stage_changed',
'deal_won',
'deal_lost',
'ticket_created',
'ticket_resolved',
'invoice_issued',
'invoice_paid',
'activity_done',
] as const;
const ACTIONS = [
'create_activity',
'create_notification',
+2 -1
View File
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { LoyaltyModule } from '../loyalty/loyalty.module';
import { AutomationsModule } from '../automations/automations.module';
import { CompaniesController } from './companies.controller';
import { CompaniesService } from './companies.service';
import { PeopleController } from './people.controller';
@@ -7,7 +8,7 @@ import { PeopleService } from './people.service';
import { ContactsExcelService } from './contacts-excel.service';
@Module({
imports: [LoyaltyModule],
imports: [LoyaltyModule, AutomationsModule],
controllers: [PeopleController, CompaniesController],
providers: [PeopleService, CompaniesService, ContactsExcelService],
exports: [PeopleService, CompaniesService],
+11
View File
@@ -25,6 +25,7 @@ import {
resolveOwnerId,
} from '../common/record-scope';
import { LoyaltyService } from '../loyalty/loyalty.service';
import { AutomationsService } from '../automations/automations.service';
const personInclude = {
company: { select: { id: true, name: true } },
@@ -64,6 +65,7 @@ export class PeopleService {
private readonly prisma: PrismaService,
private readonly companies: CompaniesService,
private readonly loyalty: LoyaltyService,
private readonly automations: AutomationsService,
) {}
async list(
@@ -149,6 +151,15 @@ export class PeopleService {
include: personInclude,
});
await this.loyalty.applyWelcomePoints(actor.organizationId, row.id);
await this.automations.runTrigger('person_created', {
organizationId: actor.organizationId,
entityType: 'person',
entityId: row.id,
title: `${row.firstName} ${row.lastName}`.trim(),
personId: row.id,
companyId: row.companyId,
ownerId: row.ownerId,
});
const fresh = await this.prisma.person.findFirst({
where: { id: row.id },
include: personInclude,
+2
View File
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { AutomationsModule } from '../automations/automations.module';
import { InvoicesController } from './invoices.controller';
import { InvoicesService } from './invoices.service';
@Module({
imports: [AutomationsModule],
controllers: [InvoicesController],
providers: [InvoicesService],
exports: [InvoicesService],
+47 -4
View File
@@ -16,6 +16,7 @@ import type {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { type Actor, ownerWhere } from '../common/record-scope';
import { AutomationsService } from '../automations/automations.service';
type InvoiceActor = Pick<Actor, 'userId' | 'role'>;
@@ -37,7 +38,10 @@ type InvoiceRow = Prisma.InvoiceGetPayload<{ include: typeof include }>;
@Injectable()
export class InvoicesService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly automations: AutomationsService,
) {}
async list(
organizationId: string,
@@ -264,7 +268,11 @@ export class InvoicesService {
},
include,
});
return this.toDto(row);
const created = this.toDto(row);
if (created.status !== 'draft' && created.status !== 'canceled') {
await this.fireIssued(organizationId, created);
}
return created;
}
async update(
@@ -374,7 +382,15 @@ export class InvoicesService {
await this.recomputeStatus(id);
}
return this.get(organizationId, id, actor);
const row = await this.get(organizationId, id, actor);
const leftDraft =
existing.status === 'draft' &&
row.status !== 'draft' &&
row.status !== 'canceled';
if (leftDraft) {
await this.fireIssued(organizationId, row);
}
return row;
}
async remove(
@@ -443,7 +459,21 @@ export class InvoicesService {
});
await this.recomputeStatus(invoiceId);
return this.get(organizationId, invoiceId, actor);
const row = await this.get(organizationId, invoiceId, actor);
if (invoice.status === 'draft') {
await this.fireIssued(organizationId, row);
}
await this.automations.runTrigger('invoice_paid', {
organizationId,
entityType: 'invoice',
entityId: row.id,
title: row.title || row.number,
personId: row.person?.id ?? null,
companyId: row.company?.id ?? null,
dealId: row.deal?.id ?? null,
ownerId: row.owner?.id ?? null,
});
return row;
}
async removePayment(
@@ -549,6 +579,19 @@ export class InvoicesService {
return `FA-${String(count + 1).padStart(4, '0')}`;
}
private async fireIssued(organizationId: string, row: InvoiceDto) {
await this.automations.runTrigger('invoice_issued', {
organizationId,
entityType: 'invoice',
entityId: row.id,
title: row.title || row.number,
personId: row.person?.id ?? null,
companyId: row.company?.id ?? null,
dealId: row.deal?.id ?? null,
ownerId: row.owner?.id ?? null,
});
}
private async ensureExists(
organizationId: string,
id: string,
+2 -1
View File
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { AutomationsModule } from '../automations/automations.module';
import { LeadFormsController } from './lead-forms.controller';
import { LeadFormsService } from './lead-forms.service';
import { PublicLeadFormsController } from './public-lead-forms.controller';
@Module({
imports: [AuthModule],
imports: [AuthModule, AutomationsModule],
controllers: [LeadFormsController, PublicLeadFormsController],
providers: [LeadFormsService],
exports: [LeadFormsService],
@@ -14,6 +14,7 @@ import type {
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { LocalFileStorage } from '../storage/local-file.storage';
import { AutomationsService } from '../automations/automations.service';
import {
emptyToNull,
normalizeEmail,
@@ -66,6 +67,7 @@ export class LeadFormsService {
constructor(
private readonly prisma: PrismaService,
private readonly files: LocalFileStorage,
private readonly automations: AutomationsService,
) {}
async list(organizationId: string): Promise<LeadFormDto[]> {
@@ -275,6 +277,19 @@ export class LeadFormsService {
});
}
const personName = [mapped.firstName, mapped.lastName]
.filter((p) => p?.trim())
.join(' ')
.trim();
await this.automations.runTrigger('lead_submitted', {
organizationId: form.organizationId,
entityType: 'person',
entityId: personId ?? form.id,
title: personName || form.name,
personId,
ownerId: ownerId ?? actorId,
});
return {
ok: true,
thankYouMessage: form.thankYouMessage,
+22 -4
View File
@@ -147,6 +147,17 @@ export class DealsService {
include: dealInclude,
});
await this.automations.runTrigger('deal_created', {
organizationId,
entityType: 'deal',
entityId: created.id,
title: created.title,
personId: created.personId,
companyId: created.companyId,
dealId: created.id,
ownerId: created.ownerId,
});
return this.pipelines.toDealDto(created);
}
@@ -290,17 +301,24 @@ export class DealsService {
});
}
if (status === 'won' && deal.status !== 'won') {
await this.automations.runTrigger('deal_won', {
if (deal.stageId !== stage.id) {
const ctx = {
organizationId,
entityType: 'deal',
entityType: 'deal' as const,
entityId: deal.id,
title: deal.title,
personId: deal.personId,
companyId: deal.companyId,
dealId: deal.id,
ownerId: deal.ownerId,
});
};
await this.automations.runTrigger('deal_stage_changed', ctx);
if (status === 'won' && deal.status !== 'won') {
await this.automations.runTrigger('deal_won', ctx);
}
if (status === 'lost' && deal.status !== 'lost') {
await this.automations.runTrigger('deal_lost', ctx);
}
}
return this.pipelines.toDealDto(row);
+12
View File
@@ -228,6 +228,18 @@ export class TicketsService {
});
}
await this.automations.runTrigger('ticket_created', {
organizationId: actor.organizationId,
entityType: 'ticket',
entityId: row.id,
title: row.subject,
personId: row.personId,
companyId: row.companyId,
dealId: row.dealId,
assigneeId: row.assigneeId,
ownerId: row.assigneeId,
});
return this.toDto(row);
}
@@ -221,6 +221,8 @@ function nodeClass(step: AutomationStep) {
<style scoped>
.canvas {
position: relative;
width: 100%;
min-width: 0;
min-height: 22rem;
border-radius: 16px;
overflow: auto;
@@ -17,8 +17,8 @@ import {
AUTOMATION_CONDITION_OP_LABELS,
AUTOMATION_NOTIFY_CHANNEL_LABELS,
AUTOMATION_NOTIFY_CHANNELS,
AUTOMATION_TRIGGER_GROUPS,
AUTOMATION_TRIGGER_LABELS,
AUTOMATION_TRIGGERS,
conditionFieldsForTrigger,
isAutomationCondition,
} from '@mento/shared';
@@ -104,9 +104,15 @@ watch(
<label>
اگر
<select v-model="trigger">
<option v-for="t in AUTOMATION_TRIGGERS" :key="t" :value="t">
{{ AUTOMATION_TRIGGER_LABELS[t] }}
</option>
<optgroup
v-for="group in AUTOMATION_TRIGGER_GROUPS"
:key="group.label"
:label="group.label"
>
<option v-for="t in group.items" :key="t" :value="t">
{{ AUTOMATION_TRIGGER_LABELS[t] }}
</option>
</optgroup>
</select>
</label>
</template>
+12 -4
View File
@@ -40,7 +40,6 @@ const salesPaths = ['/quotes', '/invoices', '/sales/forecast', '/reports', '/not
const morePaths = ['/activities/calendar', '/projects', '/project-cards', '/loyalty'];
const settingsPaths = [
'/settings/team',
'/settings/automations',
'/settings/lead-forms',
'/settings/custom-fields',
'/settings/products',
@@ -59,6 +58,7 @@ const salesActive = computed(() => pathIn(salesPaths));
const moreActive = computed(() => pathIn(morePaths));
const settingsActive = computed(() => pathIn(settingsPaths));
const productsActive = computed(() => route.path.startsWith('/settings/products'));
const automationsActive = computed(() => route.path.startsWith('/settings/automations'));
const extraActive = computed(() =>
pathIn([
'/quotes',
@@ -66,7 +66,6 @@ const extraActive = computed(() =>
'/sales/forecast',
'/notes',
'/tickets',
'/settings/automations',
'/settings/lead-forms',
'/settings/custom-fields',
'/settings/currencies',
@@ -348,6 +347,17 @@ onUnmounted(() => {
<UiIcon name="settings" :size="collapsed ? 20 : 18" />
<span>تنظیمات</span>
</RouterLink>
<RouterLink
v-if="canSettings"
to="/settings/automations"
class="nav-item"
title="اتوماسیون"
:class="{ 'router-link-active': automationsActive }"
@click="closeMenu"
>
<UiIcon name="layers" :size="collapsed ? 20 : 18" />
<span>اتوماسیون</span>
</RouterLink>
</div>
<div v-if="collapsed" ref="moreBtnRef" class="nav-more desktop-only">
@@ -419,7 +429,6 @@ onUnmounted(() => {
</button>
<div v-show="settingsOpen" class="nav-sub">
<RouterLink to="/settings/team" @click="closeMenu">تیم و نقش‌ها</RouterLink>
<RouterLink to="/settings/automations" @click="closeMenu">اتوماسیون</RouterLink>
<RouterLink to="/settings/lead-forms" @click="closeMenu">فرم‌ساز سرنخ</RouterLink>
<RouterLink to="/settings/custom-fields" @click="closeMenu">فیلدهای سفارشی</RouterLink>
<RouterLink to="/settings/currencies" @click="closeMenu">ارزها</RouterLink>
@@ -477,7 +486,6 @@ onUnmounted(() => {
<RouterLink to="/loyalty" @click="closeMenu">باشگاه مشتریان</RouterLink>
<template v-if="canSettings">
<p>تنظیمات</p>
<RouterLink to="/settings/automations" @click="closeMenu">اتوماسیون</RouterLink>
<RouterLink to="/settings/lead-forms" @click="closeMenu">فرم‌ساز سرنخ</RouterLink>
<RouterLink to="/settings/custom-fields" @click="closeMenu">فیلدهای سفارشی</RouterLink>
<RouterLink to="/settings/currencies" @click="closeMenu">ارزها</RouterLink>
+55 -10
View File
@@ -61,14 +61,15 @@ function makeStep(action: AutomationAction): AutomationStep {
}
function makeCondition(): AutomationStep {
const field =
trigger.value === 'deal_won' ? 'deal_amount' : 'ticket_priority';
const field = conditionFieldsForTrigger(trigger.value)[0] ?? 'has_person';
return {
id: `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
kind: 'condition',
conditionField: field,
conditionOp: field === 'deal_amount' ? 'gte' : 'eq',
conditionValue: field === 'deal_amount' ? 0 : 'normal',
conditionOp:
field === 'deal_amount' ? 'gte' : field === 'ticket_priority' ? 'eq' : 'is_set',
conditionValue:
field === 'deal_amount' ? 0 : field === 'ticket_priority' ? 'normal' : undefined,
};
}
@@ -256,12 +257,23 @@ onMounted(async () => {
watch(trigger, (next) => {
const allowed = conditionFieldsForTrigger(next);
const fallback = allowed[0] ?? 'has_person';
for (const step of steps.value) {
if (!isAutomationCondition(step)) continue;
if (step.conditionField && allowed.includes(step.conditionField)) continue;
step.conditionField = next === 'deal_won' ? 'deal_amount' : 'ticket_priority';
step.conditionOp = next === 'deal_won' ? 'gte' : 'eq';
step.conditionValue = next === 'deal_won' ? 0 : 'normal';
step.conditionField = fallback;
step.conditionOp =
fallback === 'deal_amount'
? 'gte'
: fallback === 'ticket_priority'
? 'eq'
: 'is_set';
step.conditionValue =
fallback === 'deal_amount'
? 0
: fallback === 'ticket_priority'
? 'normal'
: undefined;
}
});
</script>
@@ -398,6 +410,7 @@ h1 {
grid-template-columns: 1fr;
gap: 0.9rem;
align-items: start;
min-width: 0;
}
.rules,
@@ -524,11 +537,16 @@ h2 {
.stage {
display: grid;
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
min-width: 0;
}
.stage :deep(.canvas) {
width: 100%;
min-width: 0;
}
.btn {
display: inline-flex;
align-items: center;
@@ -573,11 +591,38 @@ h2 {
@media (min-width: 1280px) {
.workspace {
grid-template-columns: minmax(15rem, 17.5rem) minmax(0, 1fr);
grid-template-columns: minmax(15rem, 20rem) minmax(0, 1fr);
grid-template-areas:
'rules toolbar'
'stage stage';
}
.rules {
grid-area: rules;
height: 100%;
}
.studio {
display: contents;
border: 0;
padding: 0;
box-shadow: none;
background: transparent;
}
.toolbar {
grid-area: toolbar;
border: 1px solid #e2e8f0;
border-radius: 18px;
background: #fff;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
padding: 0.85rem 1rem;
align-items: end;
}
.stage {
grid-template-columns: minmax(0, 1fr) minmax(16rem, 19rem);
grid-area: stage;
grid-template-columns: minmax(0, 1fr);
}
}
+54 -3
View File
@@ -1722,15 +1722,35 @@ export interface RedeemLoyaltyRewardRequest {
}
/** اتوماسیون اگر/آنگاه */
export type AutomationTrigger = "deal_won" | "ticket_resolved";
export type AutomationTrigger =
| "person_created"
| "lead_submitted"
| "deal_created"
| "deal_stage_changed"
| "deal_won"
| "deal_lost"
| "ticket_created"
| "ticket_resolved"
| "invoice_issued"
| "invoice_paid"
| "activity_done";
export type AutomationAction =
| "create_activity"
| "create_notification"
| "add_loyalty_points";
export const AUTOMATION_TRIGGER_LABELS: Record<AutomationTrigger, string> = {
person_created: "مخاطب جدید ساخته شد",
lead_submitted: "فرم سرنخ ارسال شد",
deal_created: "معامله جدید ساخته شد",
deal_stage_changed: "مرحله معامله عوض شد",
deal_won: "معامله برنده شد",
deal_lost: "معامله باخت",
ticket_created: "تیکت جدید ساخته شد",
ticket_resolved: "تیکت حل شد",
invoice_issued: "فاکتور صادر شد",
invoice_paid: "پرداخت روی فاکتور ثبت شد",
activity_done: "فعالیت تمام شد",
};
export const AUTOMATION_ACTION_LABELS: Record<AutomationAction, string> = {
@@ -1748,10 +1768,33 @@ export const AUTOMATION_ACTIONS = [
] as const satisfies readonly AutomationAction[];
export const AUTOMATION_TRIGGERS = [
"person_created",
"lead_submitted",
"deal_created",
"deal_stage_changed",
"deal_won",
"deal_lost",
"ticket_created",
"ticket_resolved",
"invoice_issued",
"invoice_paid",
"activity_done",
] as const satisfies readonly AutomationTrigger[];
export const AUTOMATION_TRIGGER_GROUPS: {
label: string;
items: AutomationTrigger[];
}[] = [
{ label: "مخاطب", items: ["person_created", "lead_submitted"] },
{
label: "معامله",
items: ["deal_created", "deal_stage_changed", "deal_won", "deal_lost"],
},
{ label: "تیکت", items: ["ticket_created", "ticket_resolved"] },
{ label: "فعالیت", items: ["activity_done"] },
{ label: "مالی", items: ["invoice_issued", "invoice_paid"] },
];
export type AutomationStepKind = "action" | "condition";
export type AutomationConditionField =
@@ -1822,10 +1865,18 @@ export function firstAutomationAction(
export function conditionFieldsForTrigger(
trigger: AutomationTrigger,
): AutomationConditionField[] {
if (trigger === "deal_won") {
if (
trigger === "deal_won" ||
trigger === "deal_lost" ||
trigger === "deal_created" ||
trigger === "deal_stage_changed"
) {
return ["deal_amount", "has_person", "has_company"];
}
return ["ticket_priority", "has_person", "has_company"];
if (trigger === "ticket_resolved" || trigger === "ticket_created") {
return ["ticket_priority", "has_person", "has_company"];
}
return ["has_person", "has_company"];
}
export interface AutomationStep {