2026-09-21 18:27:37 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-09-21 20:04:18 +03:30
|
|
|
|
import { computed, nextTick, ref } from 'vue';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
|
import type { CreatePersonRequest, PersonDto } from '@mento/shared';
|
|
|
|
|
|
import { toFaDigits } from '@/lib/jalali';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Avatar as AAvatar,
|
|
|
|
|
|
Button as AButton,
|
|
|
|
|
|
Checkbox as ACheckbox,
|
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
|
Input as AInput,
|
|
|
|
|
|
List as AList,
|
|
|
|
|
|
ListItem as AListItem,
|
|
|
|
|
|
Modal as AModal,
|
|
|
|
|
|
Space as ASpace,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Upload as AUpload,
|
|
|
|
|
|
} from 'ant-design-vue';
|
|
|
|
|
|
import type { UploadProps } from 'ant-design-vue';
|
|
|
|
|
|
import { PhoneOutlined, SearchOutlined, UserAddOutlined } from '@ant-design/icons-vue';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
import {
|
2026-09-21 20:26:19 +03:30
|
|
|
|
filterPhoneContacts,
|
2026-09-21 18:27:37 +03:30
|
|
|
|
isContactPickerSupported,
|
|
|
|
|
|
parseVCardFile,
|
|
|
|
|
|
pickPhoneContacts,
|
2026-09-21 20:26:19 +03:30
|
|
|
|
sortPhoneContacts,
|
2026-09-21 18:27:37 +03:30
|
|
|
|
type PhoneContactDraft,
|
|
|
|
|
|
} from '@/lib/phone-contacts';
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const { Text: ATypographyText, Paragraph: ATypographyParagraph } = Typography;
|
|
|
|
|
|
|
2026-09-27 19:51:02 +03:30
|
|
|
|
const props = withDefaults(
|
|
|
|
|
|
defineProps<{
|
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
|
/** people = اشخاص | leads = لیدها */
|
|
|
|
|
|
target?: 'people' | 'leads';
|
|
|
|
|
|
}>(),
|
|
|
|
|
|
{ target: 'people' },
|
|
|
|
|
|
);
|
2026-09-21 18:27:37 +03:30
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
start: [];
|
|
|
|
|
|
done: [result: { created: number; skipped: number; failed: number }];
|
|
|
|
|
|
fail: [message: string];
|
|
|
|
|
|
settled: [];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const pickerSupported = isContactPickerSupported();
|
2026-09-21 20:04:18 +03:30
|
|
|
|
/** یک مودال با دو مرحله تا بعد از انتخاب فایل، لیست تأیید گم نشود */
|
2026-09-21 20:51:06 +03:30
|
|
|
|
const phase = ref<'closed' | 'choose' | 'help' | 'review'>('closed');
|
2026-09-21 18:27:37 +03:30
|
|
|
|
const importing = ref(false);
|
|
|
|
|
|
const drafts = ref<PhoneContactDraft[]>([]);
|
|
|
|
|
|
const selected = ref<Set<string>>(new Set());
|
2026-09-21 20:26:19 +03:30
|
|
|
|
const listQuery = ref('');
|
2026-09-21 18:27:37 +03:30
|
|
|
|
|
2026-09-21 20:04:18 +03:30
|
|
|
|
const modalOpen = computed(() => phase.value !== 'closed');
|
2026-09-21 20:51:06 +03:30
|
|
|
|
const modalTitle = computed(() => {
|
|
|
|
|
|
if (phase.value === 'review') return 'انتخاب مخاطبین برای ورود';
|
|
|
|
|
|
if (phase.value === 'choose') return 'ورود از مخاطبین گوشی';
|
|
|
|
|
|
return 'ورود از فایل مخاطبین';
|
|
|
|
|
|
});
|
2026-09-21 20:26:19 +03:30
|
|
|
|
const visibleDrafts = computed(() =>
|
|
|
|
|
|
filterPhoneContacts(drafts.value, listQuery.value),
|
|
|
|
|
|
);
|
2026-09-21 18:27:37 +03:30
|
|
|
|
const selectedCount = computed(
|
|
|
|
|
|
() => drafts.value.filter((d) => selected.value.has(d.key)).length,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
function apiMessage(e: unknown, fallback: string) {
|
|
|
|
|
|
if (axios.isAxiosError(e)) {
|
|
|
|
|
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
|
|
|
|
|
return Array.isArray(msg) ? msg.join('، ') : (msg ?? fallback);
|
|
|
|
|
|
}
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 20:04:18 +03:30
|
|
|
|
async function showReview(rows: PhoneContactDraft[], opts?: { selectAll?: boolean }) {
|
2026-09-21 20:26:19 +03:30
|
|
|
|
drafts.value = sortPhoneContacts(rows);
|
|
|
|
|
|
listQuery.value = '';
|
|
|
|
|
|
selected.value = opts?.selectAll ? new Set(drafts.value.map((r) => r.key)) : new Set();
|
2026-09-21 20:04:18 +03:30
|
|
|
|
phase.value = 'closed';
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
phase.value = 'review';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggle(key: string) {
|
|
|
|
|
|
const next = new Set(selected.value);
|
|
|
|
|
|
if (next.has(key)) next.delete(key);
|
|
|
|
|
|
else next.add(key);
|
|
|
|
|
|
selected.value = next;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectAll(on: boolean) {
|
2026-09-21 20:26:19 +03:30
|
|
|
|
if (!on) {
|
|
|
|
|
|
selected.value = new Set();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (listQuery.value.trim()) {
|
|
|
|
|
|
const next = new Set(selected.value);
|
|
|
|
|
|
for (const row of visibleDrafts.value) next.add(row.key);
|
|
|
|
|
|
selected.value = next;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selected.value = new Set(drafts.value.map((d) => d.key));
|
|
|
|
|
|
}
|
2026-09-21 18:27:37 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAll() {
|
2026-09-21 20:04:18 +03:30
|
|
|
|
phase.value = 'closed';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
drafts.value = [];
|
|
|
|
|
|
selected.value = new Set();
|
2026-09-21 20:26:19 +03:30
|
|
|
|
listQuery.value = '';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
importing.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancel() {
|
|
|
|
|
|
closeAll();
|
|
|
|
|
|
emit('settled');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 20:04:18 +03:30
|
|
|
|
function openHelp() {
|
|
|
|
|
|
phase.value = 'help';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 20:51:06 +03:30
|
|
|
|
function openChoose() {
|
|
|
|
|
|
phase.value = 'choose';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runDevicePicker() {
|
2026-09-21 18:27:37 +03:30
|
|
|
|
try {
|
2026-09-21 20:04:18 +03:30
|
|
|
|
const rows = await pickPhoneContacts({ multiple: true });
|
|
|
|
|
|
if (!rows.length) {
|
|
|
|
|
|
emit('fail', 'مخاطبی انتخاب نشد.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-21 20:51:06 +03:30
|
|
|
|
await showReview(rows, { selectAll: false });
|
2026-09-21 18:27:37 +03:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const name = e instanceof DOMException ? e.name : '';
|
|
|
|
|
|
const message = e instanceof Error ? e.message : '';
|
2026-09-21 20:04:18 +03:30
|
|
|
|
if (name === 'AbortError') return;
|
2026-09-21 18:27:37 +03:30
|
|
|
|
if (message === 'CONTACT_PICKER_UNSUPPORTED' || name === 'NotSupportedError') {
|
2026-09-21 20:04:18 +03:30
|
|
|
|
openHelp();
|
2026-09-21 18:27:37 +03:30
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
emit('fail', 'انتخاب مخاطب از گوشی ناموفق بود.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 20:51:06 +03:30
|
|
|
|
function startPicker() {
|
|
|
|
|
|
if (pickerSupported) {
|
|
|
|
|
|
openChoose();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
openHelp();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const beforeVcfUpload: UploadProps['beforeUpload'] = (file) => {
|
|
|
|
|
|
void handleVcfFile(file as File);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
async function handleVcfFile(file: File) {
|
2026-09-21 18:27:37 +03:30
|
|
|
|
try {
|
|
|
|
|
|
const rows = await parseVCardFile(file);
|
2026-09-21 20:04:18 +03:30
|
|
|
|
if (!rows.length) {
|
|
|
|
|
|
emit('fail', 'مخاطبی برای ورود پیدا نشد.');
|
2026-09-21 20:51:06 +03:30
|
|
|
|
phase.value = pickerSupported ? 'choose' : 'help';
|
2026-09-21 20:04:18 +03:30
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await showReview(rows, { selectAll: false });
|
2026-09-21 18:27:37 +03:30
|
|
|
|
} catch {
|
|
|
|
|
|
emit('fail', 'خواندن فایل مخاطبین (.vcf) ناموفق بود.');
|
2026-09-21 20:51:06 +03:30
|
|
|
|
phase.value = pickerSupported ? 'choose' : 'help';
|
2026-09-21 18:27:37 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function draftLine(row: PhoneContactDraft) {
|
|
|
|
|
|
return [row.mobile, row.phone, row.email].filter(Boolean).join(' · ') || 'بدون شماره/ایمیل';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function importSelected() {
|
|
|
|
|
|
const rows = drafts.value.filter((d) => selected.value.has(d.key));
|
|
|
|
|
|
if (!rows.length || importing.value) return;
|
|
|
|
|
|
importing.value = true;
|
2026-09-21 20:04:18 +03:30
|
|
|
|
emit('start');
|
2026-09-21 18:27:37 +03:30
|
|
|
|
let created = 0;
|
|
|
|
|
|
let skipped = 0;
|
|
|
|
|
|
let failed = 0;
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
|
try {
|
2026-09-27 19:51:02 +03:30
|
|
|
|
if (props.target === 'leads') {
|
|
|
|
|
|
const mobile = row.mobile || row.phone;
|
|
|
|
|
|
if (!mobile) {
|
|
|
|
|
|
failed += 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
await api.post('/leads', {
|
|
|
|
|
|
title: row.fullName,
|
|
|
|
|
|
contactName: row.fullName,
|
|
|
|
|
|
mobile,
|
|
|
|
|
|
phone: row.phone && row.phone !== mobile ? row.phone : null,
|
|
|
|
|
|
source: 'مخاطبین گوشی',
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const body: CreatePersonRequest = {
|
|
|
|
|
|
firstName: row.firstName,
|
|
|
|
|
|
lastName: row.lastName || undefined,
|
|
|
|
|
|
mobile: row.mobile,
|
|
|
|
|
|
phone: row.phone,
|
|
|
|
|
|
email: row.email,
|
2026-10-04 12:01:21 +03:30
|
|
|
|
importSource: 'phone',
|
2026-09-27 19:51:02 +03:30
|
|
|
|
};
|
|
|
|
|
|
await api.post<PersonDto>('/people', body);
|
|
|
|
|
|
}
|
2026-09-21 18:27:37 +03:30
|
|
|
|
created += 1;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (axios.isAxiosError(e) && e.response?.status === 409) {
|
|
|
|
|
|
skipped += 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failed += 1;
|
|
|
|
|
|
if (failed === 1) {
|
|
|
|
|
|
emit('fail', apiMessage(e, `ورود «${row.fullName}» ناموفق بود`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
emit('done', { created, skipped, failed });
|
|
|
|
|
|
closeAll();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
importing.value = false;
|
|
|
|
|
|
emit('settled');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<span @click.stop>
|
|
|
|
|
|
<span @click="startPicker">
|
2026-09-21 18:27:37 +03:30
|
|
|
|
<slot />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-modal
|
|
|
|
|
|
:open="modalOpen"
|
|
|
|
|
|
:title="modalTitle"
|
|
|
|
|
|
:confirm-loading="importing"
|
|
|
|
|
|
@cancel="cancel"
|
|
|
|
|
|
>
|
2026-09-21 20:51:06 +03:30
|
|
|
|
<template v-if="phase === 'choose'">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-typography-paragraph type="secondary">
|
2026-09-21 20:51:06 +03:30
|
|
|
|
مخاطبین را از دفترچه تلفن یا فایل <strong>.vcf</strong> بگیرید.
|
|
|
|
|
|
بعد در لیست الفبایی جستجو کنید و یک، چند، یا همه را برای ورود انتخاب کنید.
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
|
<a-flex vertical :gap="8">
|
|
|
|
|
|
<a-button type="primary" block @click="runDevicePicker">
|
|
|
|
|
|
<template #icon><PhoneOutlined /></template>
|
2026-09-21 20:51:06 +03:30
|
|
|
|
از دفترچه تلفن
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-upload
|
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
|
accept=".vcf,text/vcard,text/x-vcard"
|
|
|
|
|
|
:before-upload="beforeVcfUpload"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button block>از فایل .vcf</a-button>
|
|
|
|
|
|
</a-upload>
|
|
|
|
|
|
</a-flex>
|
2026-09-21 20:51:06 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="phase === 'help'">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-typography-paragraph type="secondary">
|
2026-09-21 20:04:18 +03:30
|
|
|
|
در این مرورگر انتخاب مستقیم از دفترچه تلفن پشتیبانی نمیشود.
|
|
|
|
|
|
از Contacts گوشی یک فایل <strong>.vcf</strong> خروجی بگیرید و اینجا بارگذاری کنید.
|
|
|
|
|
|
بعد از بارگذاری میتوانید یک، چند، یا همه مخاطبین را انتخاب کنید.
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
|
<a-typography-paragraph type="secondary">
|
|
|
|
|
|
اندروید Chrome معمولاً انتخاب مستقیم مخاطب را دارد. آیفون: Contacts → Share Contact /
|
|
|
|
|
|
Export → فایل vCard.
|
|
|
|
|
|
</a-typography-paragraph>
|
2026-09-21 20:04:18 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="phase === 'review'">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-typography-paragraph type="secondary">
|
2026-09-21 20:26:19 +03:30
|
|
|
|
{{ toFaDigits(drafts.length) }} مخاطب از فایل/گوشی خوانده شد (مرتبشده الفبایی).
|
2026-09-21 20:04:18 +03:30
|
|
|
|
موارد دلخواه را تیک بزنید؛ فقط انتخابشدهها وارد میشوند.
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-typography-paragraph>
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="listQuery"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="جستجو نام یا موبایل…"
|
|
|
|
|
|
style="width: 100%; margin-bottom: 12px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix><SearchOutlined /></template>
|
|
|
|
|
|
</a-input>
|
|
|
|
|
|
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
|
|
|
|
|
<a-space :size="8">
|
|
|
|
|
|
<a-button type="link" @click="selectAll(true)">
|
|
|
|
|
|
{{ listQuery.trim() ? 'انتخاب نتایج' : 'انتخاب همه' }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button type="link" @click="selectAll(false)">هیچکدام</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
<a-typography-text type="secondary">
|
2026-09-21 20:26:19 +03:30
|
|
|
|
{{ toFaDigits(selectedCount) }} انتخاب
|
|
|
|
|
|
<template v-if="listQuery.trim()">
|
|
|
|
|
|
· {{ toFaDigits(visibleDrafts.length) }} نتیجه
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
از {{ toFaDigits(drafts.length) }}
|
|
|
|
|
|
</template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
<a-list
|
|
|
|
|
|
v-if="visibleDrafts.length"
|
|
|
|
|
|
:data-source="visibleDrafts"
|
|
|
|
|
|
:style="{ maxHeight: 'min(48vh, 360px)', overflow: 'auto' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #renderItem="{ item }">
|
|
|
|
|
|
<a-list-item>
|
|
|
|
|
|
<a-checkbox
|
|
|
|
|
|
:checked="selected.has(item.key)"
|
2026-09-21 20:04:18 +03:30
|
|
|
|
:disabled="importing"
|
2026-10-18 19:39:59 +03:30
|
|
|
|
@click.prevent="toggle(item.key)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-space :size="8">
|
|
|
|
|
|
<a-avatar>{{ item.fullName.slice(0, 1) }}</a-avatar>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<div><a-typography-text strong>{{ item.fullName }}</a-typography-text></div>
|
|
|
|
|
|
<a-typography-text type="secondary">{{ draftLine(item) }}</a-typography-text>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-checkbox>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
<a-typography-text v-else type="secondary">
|
|
|
|
|
|
مخاطبی با این جستجو پیدا نشد.
|
|
|
|
|
|
</a-typography-text>
|
2026-09-21 18:27:37 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
2026-09-21 20:51:06 +03:30
|
|
|
|
<template v-if="phase === 'choose'">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-button @click="cancel">انصراف</a-button>
|
2026-09-21 20:51:06 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="phase === 'help'">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-button :disabled="importing" @click="cancel">انصراف</a-button>
|
|
|
|
|
|
<a-upload
|
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
|
accept=".vcf,text/vcard,text/x-vcard"
|
|
|
|
|
|
:before-upload="beforeVcfUpload"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button type="primary" :disabled="importing">انتخاب فایل .vcf</a-button>
|
|
|
|
|
|
</a-upload>
|
2026-09-21 20:04:18 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="phase === 'review'">
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-button :disabled="importing" @click="cancel">انصراف</a-button>
|
|
|
|
|
|
<a-upload
|
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
|
accept=".vcf,text/vcard,text/x-vcard"
|
|
|
|
|
|
:before-upload="beforeVcfUpload"
|
|
|
|
|
|
:disabled="importing"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button :disabled="importing">فایل دیگر</a-button>
|
|
|
|
|
|
</a-upload>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="primary"
|
2026-09-21 20:04:18 +03:30
|
|
|
|
:disabled="importing || !selectedCount"
|
|
|
|
|
|
:loading="importing"
|
|
|
|
|
|
@click="importSelected"
|
|
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<template #icon><UserAddOutlined /></template>
|
2026-09-27 19:51:02 +03:30
|
|
|
|
ورود {{ toFaDigits(selectedCount) }}
|
|
|
|
|
|
{{ target === 'leads' ? 'لید' : 'نفر' }}
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-button>
|
2026-09-21 20:04:18 +03:30
|
|
|
|
</template>
|
2026-09-21 18:27:37 +03:30
|
|
|
|
</template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-modal>
|
2026-09-21 18:27:37 +03:30
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|