Redesign of the SMS automation page
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "MessageAutomationRule" ADD COLUMN IF NOT EXISTS "description" TEXT;
|
||||
@@ -1142,6 +1142,7 @@ model MessageAutomationRule {
|
||||
notifyTarget MessageAutomationTarget @default(person)
|
||||
channel MessageAutomationChannel @default(sms)
|
||||
title String
|
||||
description String?
|
||||
body String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 255 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="mentoLogoBg" x1="8" y1="8" x2="56" y2="56">
|
||||
<stop offset="0%" stop-color="#5eead4"/>
|
||||
<stop offset="100%" stop-color="#14b8a6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="56" height="56" rx="16" fill="url(#mentoLogoBg)"/>
|
||||
<rect x="14" y="16" width="8" height="32" rx="4" fill="#fff" opacity="0.95"/>
|
||||
<path d="M17 17.5c1-1.6 3.5-1.8 4.8-.5L36 30l4.3-4c1.4-1.3 3.6-1.2 4.7.4L44 27.5 33 37.5c-1.1 1-2.8 1-3.9 0L16.5 24.5 17 17.5Z" fill="#134e4a"/>
|
||||
<path d="M27.5 32 38 21.5c1.3-1.3 3.4-1.2 4.6.2L50 29.5l-2.6 2.6L40 25.5 31 34c-1 1-2.8 1-3.8-.1l-.7-.9Z" fill="#ecfdf5"/>
|
||||
<rect x="42" y="16" width="8" height="32" rx="4" fill="#134e4a"/>
|
||||
<path d="M41 17.5c1.3-1.4 3.5-1.3 4.7.2l1.6 1.8-7 6.6-2.1-2c-1.2-1.1-.9-3.2 2.8-6.6Z" fill="#ecfdf5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 888 B |
Binary file not shown.
|
After Width: | Height: | Size: 527 KiB |
@@ -32,6 +32,7 @@ defineProps<{
|
||||
| 'chevron'
|
||||
| 'star'
|
||||
| 'filter'
|
||||
| 'funnel'
|
||||
| 'lightbulb'
|
||||
| 'building'
|
||||
| 'user'
|
||||
@@ -58,7 +59,9 @@ defineProps<{
|
||||
| 'crown'
|
||||
| 'gift'
|
||||
| 'medal'
|
||||
| 'logout';
|
||||
| 'logout'
|
||||
| 'pulse'
|
||||
| 'sliders';
|
||||
size?: number;
|
||||
}>();
|
||||
</script>
|
||||
@@ -221,6 +224,9 @@ defineProps<{
|
||||
<template v-else-if="name === 'filter'">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" />
|
||||
</template>
|
||||
<template v-else-if="name === 'funnel'">
|
||||
<path d="M4 4.5h16l-6.2 8.2V19l-3.6 2v-8.3L4 4.5Z" />
|
||||
</template>
|
||||
<template v-else-if="name === 'lightbulb'">
|
||||
<path d="M9 18h6" />
|
||||
<path d="M10 21h4" />
|
||||
@@ -346,6 +352,13 @@ defineProps<{
|
||||
<path d="M16 17l5-5-5-5" />
|
||||
<path d="M21 12H9" />
|
||||
</template>
|
||||
<template v-else-if="name === 'pulse'">
|
||||
<path d="M3 12h3.2l2-6.5L12 19l2.4-7.5H21" />
|
||||
</template>
|
||||
<template v-else-if="name === 'sliders'">
|
||||
<path d="M4 7h16M4 17h16" />
|
||||
<path d="M8 4.5v5M16 14.5v5" />
|
||||
</template>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -48,9 +48,10 @@ const salesActive = computed(() =>
|
||||
const activitiesActive = computed(() => pathIn(['/activities']));
|
||||
const notesActive = computed(() => route.path === '/notes' || route.path.startsWith('/notes/'));
|
||||
const supportActive = computed(() => pathIn(['/tickets']));
|
||||
const automationsActive = computed(() => route.path.startsWith('/settings/automations'));
|
||||
const messageAutomationsActive = computed(() =>
|
||||
route.path.startsWith('/settings/message-automations'),
|
||||
const automationsActive = computed(
|
||||
() =>
|
||||
route.path.startsWith('/settings/automations') ||
|
||||
route.path.startsWith('/settings/message-automations'),
|
||||
);
|
||||
const toolsActive = computed(() =>
|
||||
pathIn(['/settings/lead-forms', '/projects', '/project-cards', '/inventory', '/cheques', '/attachments']),
|
||||
@@ -403,28 +404,24 @@ onUnmounted(() => {
|
||||
<UiIcon name="chart" :size="collapsed ? 20 : 18" />
|
||||
<span>گزارشها</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
<div
|
||||
v-if="canSettings"
|
||||
to="/settings/automations"
|
||||
class="nav-item"
|
||||
title="اتوماسیون"
|
||||
:class="{ 'router-link-active': automationsActive }"
|
||||
@click="closeHub(); closeMenu()"
|
||||
class="nav-fold"
|
||||
:class="{ active: automationsActive || openHubKey === 'automations' }"
|
||||
>
|
||||
<UiIcon name="layers" :size="collapsed ? 20 : 18" />
|
||||
<span>اتوماسیون</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="canSettings"
|
||||
to="/settings/message-automations"
|
||||
class="nav-item"
|
||||
title="پیام خودکار"
|
||||
:class="{ 'router-link-active': messageAutomationsActive }"
|
||||
@click="closeHub(); closeMenu()"
|
||||
>
|
||||
<UiIcon name="mail" :size="collapsed ? 20 : 18" />
|
||||
<span>پیام خودکار</span>
|
||||
</RouterLink>
|
||||
<button
|
||||
class="nav-item nav-fold-btn"
|
||||
type="button"
|
||||
title="اتوماسیون"
|
||||
:class="{ 'router-link-active': automationsActive || openHubKey === 'automations', 'hub-on': openHubKey === 'automations' }"
|
||||
:aria-expanded="openHubKey === 'automations'"
|
||||
@click="onHubClick('automations')"
|
||||
>
|
||||
<UiIcon name="layers" :size="collapsed ? 20 : 18" />
|
||||
<span>اتوماسیون</span>
|
||||
<i class="chev" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-sep" aria-hidden="true" />
|
||||
|
||||
@@ -3,6 +3,7 @@ export type NavHubKey =
|
||||
| 'sales'
|
||||
| 'activities'
|
||||
| 'support'
|
||||
| 'automations'
|
||||
| 'tools'
|
||||
| 'settings';
|
||||
|
||||
@@ -15,6 +16,8 @@ export type NavIconName =
|
||||
| 'document'
|
||||
| 'box'
|
||||
| 'mail'
|
||||
| 'send'
|
||||
| 'sparkle'
|
||||
| 'dollar'
|
||||
| 'target'
|
||||
| 'check'
|
||||
@@ -190,6 +193,35 @@ export const NAV_HUBS: NavHubDef[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'automations',
|
||||
title: 'اتوماسیون',
|
||||
desc: 'قوانین خودکار برای گردش کار و پیامک',
|
||||
icon: 'sparkle',
|
||||
tint: 'violet',
|
||||
shortcuts: [
|
||||
{ to: '/settings/automations', title: 'قانون گردش کار', icon: 'layers' },
|
||||
{ to: '/settings/message-automations', title: 'قانون پیامک', icon: 'send' },
|
||||
],
|
||||
cards: [
|
||||
{
|
||||
to: '/settings/automations',
|
||||
title: 'اتوماسیون گردش کار',
|
||||
desc: 'اگر این شد، آن کارها را پشتسرهم انجام بده',
|
||||
icon: 'layers',
|
||||
tint: 'violet',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
to: '/settings/message-automations',
|
||||
title: 'اتوماسیون پیامکها',
|
||||
desc: 'پیامک و ناتیف با تأخیر و تکرار',
|
||||
icon: 'send',
|
||||
tint: 'sky',
|
||||
admin: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'tools',
|
||||
title: 'ابزارها',
|
||||
@@ -292,14 +324,6 @@ export const NAV_HUBS: NavHubDef[] = [
|
||||
icon: 'lightbulb',
|
||||
tint: 'rose',
|
||||
},
|
||||
{
|
||||
to: '/settings/message-automations',
|
||||
title: 'پیام خودکار',
|
||||
desc: 'پیامک و ناتیف با زمانبندی تکرار',
|
||||
icon: 'mail',
|
||||
tint: 'sky',
|
||||
admin: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -282,7 +282,7 @@ watch(trigger, (next) => {
|
||||
<section class="page">
|
||||
<header class="page-head">
|
||||
<div class="head-copy">
|
||||
<h1>اتوماسیون</h1>
|
||||
<h1>اتوماسیون گردش کار</h1>
|
||||
<p class="lead">
|
||||
مثل یک بوم کاری: گره «اگر» را به چند عمل پشتسرهم وصل کنید.
|
||||
</p>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2314,6 +2314,16 @@ export const MESSAGE_AUTOMATION_UNIT_LABELS: Record<
|
||||
months: "ماه",
|
||||
};
|
||||
|
||||
export const MESSAGE_AUTOMATION_FIELDS = [
|
||||
{ token: "{نام}", label: "نام مشتری" },
|
||||
{ token: "{شماره}", label: "شماره موبایل" },
|
||||
{ token: "{تاریخ}", label: "تاریخ ارسال" },
|
||||
{ token: "{عنوان}", label: "عنوان رکورد" },
|
||||
{ token: "{شرکت}", label: "نام شرکت" },
|
||||
] as const;
|
||||
|
||||
export const MESSAGE_AUTOMATION_SMS_SEGMENT = 70;
|
||||
|
||||
export const MESSAGE_AUTOMATION_CHANNEL_LABELS: Record<
|
||||
MessageAutomationChannel,
|
||||
string
|
||||
@@ -2345,6 +2355,7 @@ export interface MessageAutomationRuleDto {
|
||||
notifyTarget: MessageAutomationTarget;
|
||||
channel: MessageAutomationChannel;
|
||||
title: string;
|
||||
description: string | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -2362,6 +2373,7 @@ export interface CreateMessageAutomationRuleRequest {
|
||||
notifyTarget?: MessageAutomationTarget;
|
||||
channel?: MessageAutomationChannel;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
body: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user