Promote Leads to a top-level nav hub and expire login OTP after 90s.
Leads items sit under their own hub after Customers; login OTP shows a countdown and returns to the mobile step when time runs out.
This commit is contained in:
@@ -33,7 +33,7 @@ import {
|
|||||||
} from './dto';
|
} from './dto';
|
||||||
|
|
||||||
const OTP_LENGTH = 6;
|
const OTP_LENGTH = 6;
|
||||||
const OTP_TTL_SECONDS = 5 * 60;
|
const OTP_TTL_SECONDS = 90;
|
||||||
const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
|
const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
|
||||||
const AVATAR_EXT: Record<string, string> = {
|
const AVATAR_EXT: Record<string, string> = {
|
||||||
'image/jpeg': 'jpg',
|
'image/jpeg': 'jpg',
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ const dashboardActive = computed(() => route.path === '/');
|
|||||||
const loyaltyActive = computed(() => pathIn(['/loyalty']));
|
const loyaltyActive = computed(() => pathIn(['/loyalty']));
|
||||||
const reportsActive = computed(() => pathIn(['/reports']));
|
const reportsActive = computed(() => pathIn(['/reports']));
|
||||||
const customersActive = computed(() => pathIn(['/contacts']));
|
const customersActive = computed(() => pathIn(['/contacts']));
|
||||||
|
const leadsActive = computed(() =>
|
||||||
|
pathIn(['/leads', '/settings/lead-stages', '/settings/lead-forms']),
|
||||||
|
);
|
||||||
const salesActive = computed(() =>
|
const salesActive = computed(() =>
|
||||||
pathIn([
|
pathIn([
|
||||||
'/pipeline',
|
'/pipeline',
|
||||||
'/leads',
|
|
||||||
'/deals',
|
'/deals',
|
||||||
'/quotes',
|
'/quotes',
|
||||||
'/invoices',
|
'/invoices',
|
||||||
@@ -68,7 +70,6 @@ const automationsActive = computed(
|
|||||||
);
|
);
|
||||||
const toolsActive = computed(() =>
|
const toolsActive = computed(() =>
|
||||||
pathIn([
|
pathIn([
|
||||||
'/settings/lead-forms',
|
|
||||||
'/projects',
|
'/projects',
|
||||||
'/project-cards',
|
'/project-cards',
|
||||||
'/inventory',
|
'/inventory',
|
||||||
@@ -82,7 +83,6 @@ const settingsActive = computed(() =>
|
|||||||
'/settings/team',
|
'/settings/team',
|
||||||
'/settings/audit-logs',
|
'/settings/audit-logs',
|
||||||
'/settings/public-api',
|
'/settings/public-api',
|
||||||
'/settings/lead-stages',
|
|
||||||
'/settings/custom-fields',
|
'/settings/custom-fields',
|
||||||
'/settings/currencies',
|
'/settings/currencies',
|
||||||
'/settings/tahesab',
|
'/settings/tahesab',
|
||||||
@@ -432,6 +432,21 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-fold" v-if="hubVisible.leads" :class="{ active: leadsActive || openHubKey === 'leads' }">
|
||||||
|
<button
|
||||||
|
class="nav-item nav-fold-btn"
|
||||||
|
type="button"
|
||||||
|
title="لیدها"
|
||||||
|
:class="{ 'router-link-active': leadsActive || openHubKey === 'leads', 'hub-on': openHubKey === 'leads' }"
|
||||||
|
:aria-expanded="openHubKey === 'leads'"
|
||||||
|
@click="onHubClick('leads')"
|
||||||
|
>
|
||||||
|
<UiIcon name="user-plus" :size="collapsed ? 20 : 18" />
|
||||||
|
<span>لیدها</span>
|
||||||
|
<i class="chev" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="nav-fold" v-if="hubVisible.sales" :class="{ active: salesActive || openHubKey === 'sales' }">
|
<div class="nav-fold" v-if="hubVisible.sales" :class="{ active: salesActive || openHubKey === 'sales' }">
|
||||||
<button
|
<button
|
||||||
class="nav-item nav-fold-btn"
|
class="nav-item nav-fold-btn"
|
||||||
@@ -725,6 +740,16 @@ onUnmounted(() => {
|
|||||||
<UiIcon name="users" :size="20" />
|
<UiIcon name="users" :size="20" />
|
||||||
<span>مشتریان</span>
|
<span>مشتریان</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab"
|
||||||
|
type="button"
|
||||||
|
:class="{ on: !moreOpen && (leadsActive || openHubKey === 'leads') }"
|
||||||
|
:aria-expanded="openHubKey === 'leads'"
|
||||||
|
@click="openTabHub('leads')"
|
||||||
|
>
|
||||||
|
<UiIcon name="user-plus" :size="20" />
|
||||||
|
<span>لیدها</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab"
|
class="tab"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { OrgModuleKey } from '@mento/shared';
|
|||||||
|
|
||||||
export type NavHubKey =
|
export type NavHubKey =
|
||||||
| 'customers'
|
| 'customers'
|
||||||
|
| 'leads'
|
||||||
| 'sales'
|
| 'sales'
|
||||||
| 'activities'
|
| 'activities'
|
||||||
| 'support'
|
| 'support'
|
||||||
@@ -97,6 +98,53 @@ export const NAV_HUBS: NavHubDef[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'leads',
|
||||||
|
title: 'لیدها',
|
||||||
|
desc: 'مخاطبین بالقوه قبل از تبدیل به مشتری',
|
||||||
|
icon: 'user-plus',
|
||||||
|
tint: 'sky',
|
||||||
|
shortcuts: [
|
||||||
|
{ to: '/leads', title: 'فهرست لیدها', icon: 'users', module: 'leads' },
|
||||||
|
{ to: '/leads/board', title: 'کاریز لیدها', icon: 'filter', module: 'leads' },
|
||||||
|
],
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
to: '/leads',
|
||||||
|
title: 'فهرست لیدها',
|
||||||
|
desc: 'جستجو، فیلتر، تخصیص و ایمپورت اکسل',
|
||||||
|
icon: 'users',
|
||||||
|
tint: 'sky',
|
||||||
|
module: 'leads',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/leads/board',
|
||||||
|
title: 'کاریز لیدها',
|
||||||
|
desc: 'نمای کانبان و حرکت بین مراحل',
|
||||||
|
icon: 'filter',
|
||||||
|
tint: 'violet',
|
||||||
|
module: 'leads',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/settings/lead-stages',
|
||||||
|
title: 'مراحل و وضعیتها',
|
||||||
|
desc: 'تعریف وضعیتها و ستونهای کاریز لید',
|
||||||
|
icon: 'layers',
|
||||||
|
tint: 'peach',
|
||||||
|
admin: true,
|
||||||
|
module: 'leads',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/settings/lead-forms',
|
||||||
|
title: 'فرمساز سرنخ',
|
||||||
|
desc: 'لینک عمومی، iframe و QR برای جذب سرنخ',
|
||||||
|
icon: 'document',
|
||||||
|
tint: 'mint',
|
||||||
|
admin: true,
|
||||||
|
module: 'lead_forms',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'sales',
|
key: 'sales',
|
||||||
title: 'فروش',
|
title: 'فروش',
|
||||||
@@ -111,27 +159,11 @@ export const NAV_HUBS: NavHubDef[] = [
|
|||||||
{
|
{
|
||||||
to: '/pipeline',
|
to: '/pipeline',
|
||||||
title: 'کاریز فروش',
|
title: 'کاریز فروش',
|
||||||
desc: 'سرنخها، معاملات و مراحل فروش',
|
desc: 'معاملات و مراحل فروش',
|
||||||
icon: 'filter',
|
icon: 'filter',
|
||||||
tint: 'mint',
|
tint: 'mint',
|
||||||
module: 'pipeline',
|
module: 'pipeline',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
to: '/leads',
|
|
||||||
title: 'لیدها',
|
|
||||||
desc: 'فهرست سرنخها، تخصیص و پیگیری',
|
|
||||||
icon: 'user-plus',
|
|
||||||
tint: 'sky',
|
|
||||||
module: 'leads',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: '/leads/board',
|
|
||||||
title: 'کاریز لیدها',
|
|
||||||
desc: 'نمای کانبان مراحل لید',
|
|
||||||
icon: 'filter',
|
|
||||||
tint: 'violet',
|
|
||||||
module: 'leads',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
to: '/settings/products',
|
to: '/settings/products',
|
||||||
title: 'محصولات و خدمات',
|
title: 'محصولات و خدمات',
|
||||||
@@ -284,7 +316,7 @@ export const NAV_HUBS: NavHubDef[] = [
|
|||||||
{
|
{
|
||||||
key: 'tools',
|
key: 'tools',
|
||||||
title: 'ابزارها',
|
title: 'ابزارها',
|
||||||
desc: 'فرمها، پروژه، انبار و اسناد مالی',
|
desc: 'پروژه، انبار و اسناد مالی',
|
||||||
icon: 'apps-plus',
|
icon: 'apps-plus',
|
||||||
tint: 'violet',
|
tint: 'violet',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
@@ -293,15 +325,6 @@ export const NAV_HUBS: NavHubDef[] = [
|
|||||||
{ to: '/reservations', title: 'نوبت جدید', icon: 'calendar', module: 'reservations' },
|
{ to: '/reservations', title: 'نوبت جدید', icon: 'calendar', module: 'reservations' },
|
||||||
],
|
],
|
||||||
cards: [
|
cards: [
|
||||||
{
|
|
||||||
to: '/settings/lead-forms',
|
|
||||||
title: 'فرمساز سرنخ',
|
|
||||||
desc: 'فرم عمومی برای جذب سرنخ تازه',
|
|
||||||
icon: 'layers',
|
|
||||||
tint: 'mint',
|
|
||||||
admin: true,
|
|
||||||
module: 'lead_forms',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
to: '/projects',
|
to: '/projects',
|
||||||
title: 'کاریز پروژه',
|
title: 'کاریز پروژه',
|
||||||
@@ -403,15 +426,6 @@ export const NAV_HUBS: NavHubDef[] = [
|
|||||||
module: 'public_api',
|
module: 'public_api',
|
||||||
admin: true,
|
admin: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
to: '/settings/lead-stages',
|
|
||||||
title: 'تنظیمات مراحل لید',
|
|
||||||
desc: 'وضعیتها و برچسبهای کاریز لید',
|
|
||||||
icon: 'layers',
|
|
||||||
tint: 'violet',
|
|
||||||
admin: true,
|
|
||||||
module: 'leads',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
to: '/settings/custom-fields',
|
to: '/settings/custom-fields',
|
||||||
title: 'فیلدهای سفارشی',
|
title: 'فیلدهای سفارشی',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||||
import { useRouter, useRoute, RouterLink } from 'vue-router';
|
import { useRouter, useRoute, RouterLink } from 'vue-router';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
|
import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
|
||||||
@@ -10,6 +10,7 @@ import { USER_ROLE_LABELS } from '@/lib/roles';
|
|||||||
import { brand } from '@mento/shared';
|
import { brand } from '@mento/shared';
|
||||||
import BrandName from '@/components/brand/BrandName.vue';
|
import BrandName from '@/components/brand/BrandName.vue';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -29,12 +30,42 @@ const challenge = ref<LoginChallengeResponse | null>(null);
|
|||||||
const pendingChoice = ref<LoginOrganizationChoiceResponse | null>(null);
|
const pendingChoice = ref<LoginOrganizationChoiceResponse | null>(null);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const otpSecondsLeft = ref(0);
|
||||||
|
let otpTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const otpHint = computed(() => {
|
const otpHint = computed(() => {
|
||||||
const dest = challenge.value?.maskedDestination ?? '';
|
const dest = challenge.value?.maskedDestination ?? '';
|
||||||
return dest ? `کد ارسالشده به ${dest} را وارد کنید` : 'کد یکبارمصرف را وارد کنید';
|
return dest ? `کد ارسالشده به ${dest} را وارد کنید` : 'کد یکبارمصرف را وارد کنید';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const otpTimerLabel = computed(() => {
|
||||||
|
const s = Math.max(0, otpSecondsLeft.value);
|
||||||
|
const mm = Math.floor(s / 60);
|
||||||
|
const ss = s % 60;
|
||||||
|
return `${toFaDigits(mm)}:${toFaDigits(String(ss).padStart(2, '0'))}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearOtpTimer() {
|
||||||
|
if (otpTimer) {
|
||||||
|
clearInterval(otpTimer);
|
||||||
|
otpTimer = null;
|
||||||
|
}
|
||||||
|
otpSecondsLeft.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOtpTimer(seconds: number) {
|
||||||
|
clearOtpTimer();
|
||||||
|
otpSecondsLeft.value = Math.max(1, Math.round(seconds) || 90);
|
||||||
|
otpTimer = setInterval(() => {
|
||||||
|
if (otpSecondsLeft.value <= 1) {
|
||||||
|
clearOtpTimer();
|
||||||
|
backToOtpMobile('زمان کد به پایان رسید. دوباره درخواست دهید.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otpSecondsLeft.value -= 1;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
function apiError(e: unknown, fallback: string) {
|
function apiError(e: unknown, fallback: string) {
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
if (!e.response) {
|
if (!e.response) {
|
||||||
@@ -79,6 +110,7 @@ async function submitOtpMobile() {
|
|||||||
challenge.value = await auth.requestLoginOtp(mobile.value.trim());
|
challenge.value = await auth.requestLoginOtp(mobile.value.trim());
|
||||||
if (challenge.value.devCode) otpCode.value = challenge.value.devCode;
|
if (challenge.value.devCode) otpCode.value = challenge.value.devCode;
|
||||||
otpStep.value = 'code';
|
otpStep.value = 'code';
|
||||||
|
startOtpTimer(challenge.value.expiresInSeconds ?? 90);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = apiError(e, 'ارسال کد ناموفق بود');
|
error.value = apiError(e, 'ارسال کد ناموفق بود');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -88,10 +120,15 @@ async function submitOtpMobile() {
|
|||||||
|
|
||||||
async function submitOtpCode() {
|
async function submitOtpCode() {
|
||||||
if (!challenge.value) return;
|
if (!challenge.value) return;
|
||||||
|
if (otpSecondsLeft.value <= 0) {
|
||||||
|
backToOtpMobile('زمان کد به پایان رسیده است.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
error.value = '';
|
error.value = '';
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await auth.verifyLoginOtp(challenge.value.challengeId, otpCode.value.trim());
|
const result = await auth.verifyLoginOtp(challenge.value.challengeId, otpCode.value.trim());
|
||||||
|
clearOtpTimer();
|
||||||
await afterAuth(result);
|
await afterAuth(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
error.value = apiError(e, 'تأیید کد ناموفق بود');
|
||||||
@@ -102,17 +139,19 @@ async function submitOtpCode() {
|
|||||||
|
|
||||||
function switchMode(next: 'password' | 'otp') {
|
function switchMode(next: 'password' | 'otp') {
|
||||||
mode.value = next;
|
mode.value = next;
|
||||||
|
clearOtpTimer();
|
||||||
otpStep.value = 'mobile';
|
otpStep.value = 'mobile';
|
||||||
challenge.value = null;
|
challenge.value = null;
|
||||||
otpCode.value = '';
|
otpCode.value = '';
|
||||||
error.value = '';
|
error.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function backToOtpMobile() {
|
function backToOtpMobile(message?: string) {
|
||||||
|
clearOtpTimer();
|
||||||
otpStep.value = 'mobile';
|
otpStep.value = 'mobile';
|
||||||
otpCode.value = '';
|
otpCode.value = '';
|
||||||
challenge.value = null;
|
challenge.value = null;
|
||||||
error.value = '';
|
error.value = message ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickOrganization(organizationId: string) {
|
async function pickOrganization(organizationId: string) {
|
||||||
@@ -133,6 +172,10 @@ function cancelOrgChoice() {
|
|||||||
pendingChoice.value = null;
|
pendingChoice.value = null;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearOtpTimer();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -286,8 +329,11 @@ function cancelOrgChoice() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button type="button" class="back" @click="backToOtpMobile">بازگشت</button>
|
<button type="button" class="back" @click="backToOtpMobile()">بازگشت</button>
|
||||||
<p class="otp-lead">{{ otpHint }}</p>
|
<p class="otp-lead">{{ otpHint }}</p>
|
||||||
|
<p class="otp-timer" :class="{ urgent: otpSecondsLeft <= 15 }" aria-live="polite">
|
||||||
|
اعتبار کد: <strong>{{ otpTimerLabel }}</strong>
|
||||||
|
</p>
|
||||||
<p v-if="challenge?.devCode" class="dev-hint">
|
<p v-if="challenge?.devCode" class="dev-hint">
|
||||||
کد توسعه: <strong>{{ challenge.devCode }}</strong>
|
کد توسعه: <strong>{{ challenge.devCode }}</strong>
|
||||||
</p>
|
</p>
|
||||||
@@ -309,7 +355,7 @@ function cancelOrgChoice() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<button class="btn submit" type="submit" :disabled="loading">
|
<button class="btn submit" type="submit" :disabled="loading || otpSecondsLeft <= 0">
|
||||||
{{ loading ? 'در حال تأیید…' : 'تأیید و ورود' }}
|
{{ loading ? 'در حال تأیید…' : 'تأیید و ورود' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -566,11 +612,29 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.otp-lead {
|
.otp-lead {
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 0.5rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.otp-timer {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-timer strong {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-timer.urgent {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.dev-hint {
|
.dev-hint {
|
||||||
margin: -0.4rem 0 1rem;
|
margin: -0.4rem 0 1rem;
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.65rem 0.8rem;
|
||||||
|
|||||||
@@ -204,11 +204,11 @@ export const ORG_MODULE_GROUPS: Array<{
|
|||||||
keys: OrgModuleKey[];
|
keys: OrgModuleKey[];
|
||||||
}> = [
|
}> = [
|
||||||
{ title: "مشتریان", keys: ["customers"] },
|
{ title: "مشتریان", keys: ["customers"] },
|
||||||
|
{ title: "لیدها", keys: ["leads", "lead_forms"] },
|
||||||
{
|
{
|
||||||
title: "فروش",
|
title: "فروش",
|
||||||
keys: [
|
keys: [
|
||||||
"pipeline",
|
"pipeline",
|
||||||
"leads",
|
|
||||||
"products",
|
"products",
|
||||||
"invoices",
|
"invoices",
|
||||||
"quotes",
|
"quotes",
|
||||||
@@ -229,7 +229,6 @@ export const ORG_MODULE_GROUPS: Array<{
|
|||||||
{
|
{
|
||||||
title: "ابزارها",
|
title: "ابزارها",
|
||||||
keys: [
|
keys: [
|
||||||
"lead_forms",
|
|
||||||
"project_boards",
|
"project_boards",
|
||||||
"inventory",
|
"inventory",
|
||||||
"cheques",
|
"cheques",
|
||||||
@@ -3662,7 +3661,9 @@ export interface ReservationListResponse {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ——— ãÏíÑíÊ áíÏåÇ ——— */
|
|
||||||
|
|
||||||
|
/* ——— مدیریت لیدها ——— */
|
||||||
|
|
||||||
export type LeadAssignRule = "round_robin" | "equal_split";
|
export type LeadAssignRule = "round_robin" | "equal_split";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user