Redesign of the SMS automation page

This commit is contained in:
2026-09-14 16:49:43 +03:30
parent 8925c77ae0
commit f176fde1eb
15 changed files with 2089 additions and 226 deletions
@@ -46,9 +46,16 @@ function testCancelMap() {
function testTemplate() {
assert(
applyMessageTemplate('سلام {title}', 'فاکتور ۱۲') === 'سلام فاکتور ۱۲',
applyMessageTemplate('سلام {title}', { title: 'فاکتور ۱۲' }) === 'سلام فاکتور ۱۲',
'title var',
);
assert(
applyMessageTemplate('سلام {نام} {شماره}', {
name: 'علی',
phone: '0912',
}) === 'سلام علی 0912',
'name phone vars',
);
assert(clipSms('الف'.repeat(200)).length === 160, 'sms clip');
}
+10
View File
@@ -72,6 +72,11 @@ export class CreateMessageAutomationRuleDto {
@IsIn(CHANNELS)
channel?: MessageAutomationChannel;
@IsOptional()
@IsString()
@MaxLength(500)
description?: string | null;
@IsString()
@MinLength(2)
@MaxLength(120)
@@ -135,6 +140,11 @@ export class UpdateMessageAutomationRuleDto {
@IsIn(CHANNELS)
channel?: MessageAutomationChannel;
@IsOptional()
@IsString()
@MaxLength(500)
description?: string | null;
@IsOptional()
@IsString()
@MinLength(2)
@@ -63,8 +63,32 @@ export function buildSendAts(
return times;
}
export function applyMessageTemplate(text: string, title: string): string {
return text.replace(/\{title\}/g, title).replace(/\{عنوان\}/g, title);
export function applyMessageTemplate(
text: string,
vars: {
title?: string;
name?: string;
phone?: string;
date?: string;
company?: string;
},
): string {
const map: Record<string, string> = {
title: vars.title ?? '',
عنوان: vars.title ?? '',
نام: vars.name ?? '',
name: vars.name ?? '',
شماره: vars.phone ?? '',
phone: vars.phone ?? '',
تاریخ: vars.date ?? '',
date: vars.date ?? '',
شرکت: vars.company ?? '',
company: vars.company ?? '',
};
return text.replace(/\{([^}]+)\}/g, (full, key: string) => {
const next = map[key.trim()];
return next === undefined ? full : next;
});
}
export function clipSms(text: string): string {
@@ -14,6 +14,7 @@ import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { SmsService } from '../common/sms.service';
import { dateToJalali, jalaliMonthName } from '../common/jalali';
import {
applyMessageTemplate,
buildSendAts,
@@ -97,6 +98,9 @@ export class MessageAutomationsService {
: {}),
...(dto.channel !== undefined ? { channel: dto.channel } : {}),
...(dto.title !== undefined ? { title: dto.title.trim() } : {}),
...(dto.description !== undefined
? { description: dto.description?.trim() || null }
: {}),
...(dto.body !== undefined ? { body: dto.body.trim() } : {}),
},
});
@@ -328,11 +332,33 @@ export class MessageAutomationsService {
notifyTarget: string;
};
}) {
const title = applyMessageTemplate(job.rule.title, job.entityTitle).slice(
0,
200,
);
const body = applyMessageTemplate(job.rule.body, job.entityTitle);
const person = job.personId
? await this.prisma.person.findFirst({
where: {
id: job.personId,
organizationId: job.organizationId,
},
select: {
firstName: true,
lastName: true,
mobile: true,
phone: true,
company: { select: { name: true } },
},
})
: null;
const personName = person
? [person.firstName, person.lastName].filter(Boolean).join(' ').trim()
: '';
const vars = {
title: job.entityTitle,
name: personName || job.entityTitle,
phone: person?.mobile || person?.phone || '',
date: formatJalaliDayFa(new Date()),
company: person?.company?.name ?? '',
};
const title = applyMessageTemplate(job.rule.title, vars).slice(0, 200);
const body = applyMessageTemplate(job.rule.body, vars);
const smsText = clipSms(body || title);
const wantApp = job.rule.channel === 'app' || job.rule.channel === 'both';
const wantSms = job.rule.channel === 'sms' || job.rule.channel === 'both';
@@ -366,15 +392,6 @@ export class MessageAutomationsService {
}
if (targetPerson && wantSms) {
const person = job.personId
? await this.prisma.person.findFirst({
where: {
id: job.personId,
organizationId: job.organizationId,
},
select: { mobile: true, phone: true },
})
: null;
await this.sendSms(person?.mobile || person?.phone, smsText);
}
}
@@ -413,6 +430,7 @@ export class MessageAutomationsService {
notifyTarget: dto.notifyTarget ?? 'person',
channel: dto.channel ?? 'sms',
title: dto.title.trim(),
description: dto.description?.trim() || null,
body: dto.body.trim(),
};
}
@@ -430,6 +448,7 @@ export class MessageAutomationsService {
notifyTarget: string;
channel: string;
title: string;
description?: string | null;
body: string;
createdAt: Date;
updatedAt: Date;
@@ -447,6 +466,7 @@ export class MessageAutomationsService {
notifyTarget: row.notifyTarget as MessageAutomationRuleDto['notifyTarget'],
channel: row.channel as MessageAutomationRuleDto['channel'],
title: row.title,
description: row.description ?? null,
body: row.body,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
@@ -454,6 +474,15 @@ export class MessageAutomationsService {
}
}
function toFaDigits(value: number | string) {
return String(value).replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[Number(d)]!);
}
function formatJalaliDayFa(d: Date) {
const { jy, jm, jd } = dateToJalali(d);
return `${toFaDigits(jd)} ${jalaliMonthName(jm)} ${toFaDigits(jy)}`;
}
function entityLink(entityType: string, entityId: string) {
if (entityType === 'deal') return `/deals/${entityId}`;
if (entityType === 'ticket') return `/tickets/${entityId}`;