Align Leads header with Contacts and add phone-contact lead import.
Leads actions mirror people import/export, and create-lead can fill fields from device contacts like Person form.
This commit is contained in:
@@ -15,9 +15,14 @@ import {
|
|||||||
type PhoneContactDraft,
|
type PhoneContactDraft,
|
||||||
} from '@/lib/phone-contacts';
|
} from '@/lib/phone-contacts';
|
||||||
|
|
||||||
defineProps<{
|
const props = withDefaults(
|
||||||
disabled?: boolean;
|
defineProps<{
|
||||||
}>();
|
disabled?: boolean;
|
||||||
|
/** people = اشخاص | leads = لیدها */
|
||||||
|
target?: 'people' | 'leads';
|
||||||
|
}>(),
|
||||||
|
{ target: 'people' },
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
start: [];
|
start: [];
|
||||||
@@ -171,15 +176,30 @@ async function importSelected() {
|
|||||||
let failed = 0;
|
let failed = 0;
|
||||||
try {
|
try {
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const body: CreatePersonRequest = {
|
|
||||||
firstName: row.firstName,
|
|
||||||
lastName: row.lastName || undefined,
|
|
||||||
mobile: row.mobile,
|
|
||||||
phone: row.phone,
|
|
||||||
email: row.email,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
await api.post<PersonDto>('/people', body);
|
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,
|
||||||
|
};
|
||||||
|
await api.post<PersonDto>('/people', body);
|
||||||
|
}
|
||||||
created += 1;
|
created += 1;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (axios.isAxiosError(e) && e.response?.status === 409) {
|
if (axios.isAxiosError(e) && e.response?.status === 409) {
|
||||||
@@ -315,7 +335,8 @@ async function importSelected() {
|
|||||||
@click="importSelected"
|
@click="importSelected"
|
||||||
>
|
>
|
||||||
<UiIcon name="user-plus" :size="16" />
|
<UiIcon name="user-plus" :size="16" />
|
||||||
ورود {{ toFaDigits(selectedCount) }} نفر
|
ورود {{ toFaDigits(selectedCount) }}
|
||||||
|
{{ target === 'leads' ? 'لید' : 'نفر' }}
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
@@ -16,6 +16,15 @@ import {
|
|||||||
UiPageHeader,
|
UiPageHeader,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
import PhoneContactsImport from '@/components/PhoneContactsImport.vue';
|
||||||
|
import {
|
||||||
|
filterPhoneContacts,
|
||||||
|
isContactPickerSupported,
|
||||||
|
parseVCardFile,
|
||||||
|
pickPhoneContacts,
|
||||||
|
sortPhoneContacts,
|
||||||
|
type PhoneContactDraft,
|
||||||
|
} from '@/lib/phone-contacts';
|
||||||
import type {
|
import type {
|
||||||
LeadDto,
|
LeadDto,
|
||||||
LeadImportJobDto,
|
LeadImportJobDto,
|
||||||
@@ -50,7 +59,6 @@ const cityOptions = ref<string[]>([]);
|
|||||||
const sourceOptions = ref<string[]>([]);
|
const sourceOptions = ref<string[]>([]);
|
||||||
const selected = ref<Set<string>>(new Set());
|
const selected = ref<Set<string>>(new Set());
|
||||||
const sortOpen = ref(false);
|
const sortOpen = ref(false);
|
||||||
const moreOpen = ref(false);
|
|
||||||
const rowMenuId = ref<string | null>(null);
|
const rowMenuId = ref<string | null>(null);
|
||||||
const assignOpen = ref(false);
|
const assignOpen = ref(false);
|
||||||
const stageOpen = ref(false);
|
const stageOpen = ref(false);
|
||||||
@@ -71,8 +79,19 @@ const createForm = ref({
|
|||||||
stageId: '',
|
stageId: '',
|
||||||
assigneeId: '',
|
assigneeId: '',
|
||||||
});
|
});
|
||||||
|
const contactPickerSupported = isContactPickerSupported();
|
||||||
|
const pickingContact = ref(false);
|
||||||
|
const contactFileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const sourceOpen = ref(false);
|
||||||
|
const pickOpen = ref(false);
|
||||||
|
const pickDrafts = ref<PhoneContactDraft[]>([]);
|
||||||
|
const pickQuery = ref('');
|
||||||
let importTimer: ReturnType<typeof setInterval> | null = null;
|
let importTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const visiblePickDrafts = computed(() =>
|
||||||
|
filterPhoneContacts(pickDrafts.value, pickQuery.value),
|
||||||
|
);
|
||||||
|
|
||||||
const page = computed(() => {
|
const page = computed(() => {
|
||||||
const raw = Number(route.query.page);
|
const raw = Number(route.query.page);
|
||||||
return Number.isFinite(raw) && raw > 0 ? raw : 1;
|
return Number.isFinite(raw) && raw > 0 ? raw : 1;
|
||||||
@@ -222,7 +241,6 @@ function lastActivity(lead: LeadDto) {
|
|||||||
|
|
||||||
function closePopups() {
|
function closePopups() {
|
||||||
sortOpen.value = false;
|
sortOpen.value = false;
|
||||||
moreOpen.value = false;
|
|
||||||
rowMenuId.value = null;
|
rowMenuId.value = null;
|
||||||
assignOpen.value = false;
|
assignOpen.value = false;
|
||||||
stageOpen.value = false;
|
stageOpen.value = false;
|
||||||
@@ -353,9 +371,118 @@ function openCreate() {
|
|||||||
stageId: stages.value[0]?.id ?? '',
|
stageId: stages.value[0]?.id ?? '',
|
||||||
assigneeId: '',
|
assigneeId: '',
|
||||||
};
|
};
|
||||||
|
closePick();
|
||||||
|
closeSource();
|
||||||
createOpen.value = true;
|
createOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyContactDraft(draft: PhoneContactDraft) {
|
||||||
|
if (draft.mobile) {
|
||||||
|
createForm.value.mobile = draft.mobile;
|
||||||
|
} else if (draft.phone && !createForm.value.mobile.trim()) {
|
||||||
|
createForm.value.mobile = draft.phone;
|
||||||
|
}
|
||||||
|
if (!createForm.value.contactName.trim() && draft.fullName) {
|
||||||
|
createForm.value.contactName = draft.fullName;
|
||||||
|
}
|
||||||
|
if (!createForm.value.title.trim() && draft.fullName) {
|
||||||
|
createForm.value.title = draft.fullName;
|
||||||
|
}
|
||||||
|
if (!createForm.value.source.trim()) {
|
||||||
|
createForm.value.source = 'مخاطبین گوشی';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draftLine(row: PhoneContactDraft) {
|
||||||
|
return [row.mobile, row.phone, row.email].filter(Boolean).join(' · ') || 'بدون شماره/ایمیل';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePick() {
|
||||||
|
pickOpen.value = false;
|
||||||
|
pickDrafts.value = [];
|
||||||
|
pickQuery.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSource() {
|
||||||
|
sourceOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function choosePick(row: PhoneContactDraft) {
|
||||||
|
applyContactDraft(row);
|
||||||
|
closePick();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPickList(rows: PhoneContactDraft[]) {
|
||||||
|
pickDrafts.value = sortPhoneContacts(rows);
|
||||||
|
pickQuery.value = '';
|
||||||
|
sourceOpen.value = false;
|
||||||
|
pickOpen.value = false;
|
||||||
|
await nextTick();
|
||||||
|
pickOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillFromPhoneContact() {
|
||||||
|
if (pickingContact.value || createBusy.value) return;
|
||||||
|
createError.value = '';
|
||||||
|
if (contactPickerSupported) {
|
||||||
|
sourceOpen.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contactFileInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickFromDevice() {
|
||||||
|
if (pickingContact.value || createBusy.value) return;
|
||||||
|
pickingContact.value = true;
|
||||||
|
createError.value = '';
|
||||||
|
sourceOpen.value = false;
|
||||||
|
try {
|
||||||
|
const rows = await pickPhoneContacts({ multiple: true });
|
||||||
|
if (!rows.length) {
|
||||||
|
createError.value = 'مخاطبی انتخاب نشد.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await openPickList(rows);
|
||||||
|
} catch (e) {
|
||||||
|
const name = e instanceof DOMException ? e.name : '';
|
||||||
|
const message = e instanceof Error ? e.message : '';
|
||||||
|
if (name === 'AbortError') return;
|
||||||
|
if (message === 'CONTACT_PICKER_UNSUPPORTED' || name === 'NotSupportedError') {
|
||||||
|
contactFileInput.value?.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createError.value = 'انتخاب مخاطب از گوشی ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
pickingContact.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFromVcf() {
|
||||||
|
sourceOpen.value = false;
|
||||||
|
contactFileInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onContactVcfFile(ev: Event) {
|
||||||
|
const el = ev.target as HTMLInputElement;
|
||||||
|
const file = el.files?.[0];
|
||||||
|
el.value = '';
|
||||||
|
if (!file) return;
|
||||||
|
pickingContact.value = true;
|
||||||
|
createError.value = '';
|
||||||
|
try {
|
||||||
|
const rows = await parseVCardFile(file);
|
||||||
|
if (!rows.length) {
|
||||||
|
createError.value = 'مخاطبی در فایل پیدا نشد.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await openPickList(rows);
|
||||||
|
} catch {
|
||||||
|
createError.value = 'خواندن فایل مخاطب (.vcf) ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
pickingContact.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitCreate() {
|
async function submitCreate() {
|
||||||
createError.value = '';
|
createError.value = '';
|
||||||
if (!createForm.value.title.trim() || !createForm.value.mobile.trim()) {
|
if (!createForm.value.title.trim() || !createForm.value.mobile.trim()) {
|
||||||
@@ -437,7 +564,6 @@ async function archiveSelected() {
|
|||||||
await api.post('/leads/archive', { leadIds: selectedIds.value });
|
await api.post('/leads/archive', { leadIds: selectedIds.value });
|
||||||
okMsg.value = `${toFaDigits(selectedIds.value.length)} لید بایگانی شد.`;
|
okMsg.value = `${toFaDigits(selectedIds.value.length)} لید بایگانی شد.`;
|
||||||
selected.value = new Set();
|
selected.value = new Set();
|
||||||
moreOpen.value = false;
|
|
||||||
await load();
|
await load();
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'بایگانی ناموفق بود.';
|
error.value = 'بایگانی ناموفق بود.';
|
||||||
@@ -517,6 +643,31 @@ async function onImportFile(ev: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPhoneImportStart() {
|
||||||
|
busy.value = true;
|
||||||
|
okMsg.value = '';
|
||||||
|
error.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPhoneImportDone(data: {
|
||||||
|
created: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
}) {
|
||||||
|
okMsg.value = `از گوشی وارد شد: ${toFaDigits(data.created)} · تکراری: ${toFaDigits(data.skipped)}${
|
||||||
|
data.failed ? ` · ناموفق: ${toFaDigits(data.failed)}` : ''
|
||||||
|
}`;
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPhoneImportFail(message: string) {
|
||||||
|
error.value = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPhoneImportSettled() {
|
||||||
|
busy.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function goDetail(id: string) {
|
function goDetail(id: string) {
|
||||||
void router.push({ name: 'lead-detail', params: { id } });
|
void router.push({ name: 'lead-detail', params: { id } });
|
||||||
}
|
}
|
||||||
@@ -561,68 +712,44 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
lead="مدیریت و پیگیری تمام لیدهای ورودی"
|
lead="مدیریت و پیگیری تمام لیدهای ورودی"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="header-actions">
|
<UiButton variant="ghost" size="sm" :disabled="busy" @click.stop="exportExcel">
|
||||||
<div class="pop">
|
<span class="btn-full">خروجی اکسل</span>
|
||||||
<button
|
<span class="btn-short">خروجی</span>
|
||||||
class="more-btn"
|
</UiButton>
|
||||||
type="button"
|
<UiButton
|
||||||
title="بیشتر"
|
variant="ghost"
|
||||||
@click.stop="moreOpen = !moreOpen; sortOpen = false"
|
size="sm"
|
||||||
>
|
:disabled="busy"
|
||||||
<UiIcon name="dots" :size="18" />
|
@click.stop="fileInput?.click()"
|
||||||
</button>
|
>
|
||||||
<ul v-if="moreOpen" class="menu">
|
<span class="btn-full">ورود از اکسل</span>
|
||||||
<li>
|
<span class="btn-short">ورود</span>
|
||||||
<RouterLink :to="{ name: 'leads-board' }" @click="moreOpen = false">
|
</UiButton>
|
||||||
کاریز لیدها
|
<input
|
||||||
</RouterLink>
|
ref="fileInput"
|
||||||
</li>
|
type="file"
|
||||||
<li v-if="isAdmin">
|
accept=".xlsx,.xls,.csv"
|
||||||
<RouterLink :to="{ name: 'lead-stages' }" @click="moreOpen = false">
|
hidden
|
||||||
تنظیم وضعیتها
|
@change="onImportFile"
|
||||||
</RouterLink>
|
/>
|
||||||
</li>
|
<PhoneContactsImport
|
||||||
<li v-if="selectedIds.length">
|
target="leads"
|
||||||
<button type="button" @click.stop="archiveSelected">
|
:disabled="busy"
|
||||||
بایگانی انتخابشده
|
@start="onPhoneImportStart"
|
||||||
</button>
|
@done="onPhoneImportDone"
|
||||||
</li>
|
@fail="onPhoneImportFail"
|
||||||
</ul>
|
@settled="onPhoneImportSettled"
|
||||||
</div>
|
>
|
||||||
<UiButton
|
<UiButton variant="ghost" size="sm" :disabled="busy">
|
||||||
v-if="isAdmin"
|
<UiIcon name="phone" :size="15" />
|
||||||
variant="ghost"
|
<span class="btn-full">از مخاطبین گوشی</span>
|
||||||
size="sm"
|
<span class="btn-short">گوشی</span>
|
||||||
class="excel-btn"
|
|
||||||
:disabled="busy"
|
|
||||||
@click.stop="exportExcel"
|
|
||||||
>
|
|
||||||
<UiIcon name="document" :size="16" />
|
|
||||||
خروجی Excel
|
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<UiButton
|
</PhoneContactsImport>
|
||||||
v-if="isAdmin"
|
<UiButton variant="primary" @click.stop="openCreate">
|
||||||
variant="ghost"
|
<UiIcon name="plus" :size="16" />
|
||||||
size="sm"
|
لید جدید
|
||||||
class="excel-btn"
|
</UiButton>
|
||||||
:disabled="busy"
|
|
||||||
@click.stop="fileInput?.click()"
|
|
||||||
>
|
|
||||||
<UiIcon name="box" :size="16" />
|
|
||||||
ورود Excel
|
|
||||||
</UiButton>
|
|
||||||
<input
|
|
||||||
ref="fileInput"
|
|
||||||
type="file"
|
|
||||||
accept=".xlsx,.xls,.csv"
|
|
||||||
hidden
|
|
||||||
@change="onImportFile"
|
|
||||||
/>
|
|
||||||
<UiButton variant="primary" @click.stop="openCreate">
|
|
||||||
<UiIcon name="plus" :size="16" />
|
|
||||||
ثبت لید جدید
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</UiPageHeader>
|
</UiPageHeader>
|
||||||
|
|
||||||
@@ -697,7 +824,7 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
<button
|
<button
|
||||||
class="sort-btn"
|
class="sort-btn"
|
||||||
type="button"
|
type="button"
|
||||||
@click.stop="sortOpen = !sortOpen; moreOpen = false"
|
@click.stop="sortOpen = !sortOpen"
|
||||||
>
|
>
|
||||||
<UiIcon name="trend" :size="15" />
|
<UiIcon name="trend" :size="15" />
|
||||||
{{ sortLabels[sort] }}
|
{{ sortLabels[sort] }}
|
||||||
@@ -959,8 +1086,19 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
نام مخاطب
|
نام مخاطب
|
||||||
<input v-model="createForm.contactName" placeholder="مهدی احمدی" />
|
<input v-model="createForm.contactName" placeholder="مهدی احمدی" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="mobile-field">
|
||||||
موبایل
|
<span class="label-row">
|
||||||
|
موبایل
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="from-contact"
|
||||||
|
:disabled="pickingContact || createBusy"
|
||||||
|
@click="fillFromPhoneContact"
|
||||||
|
>
|
||||||
|
<UiIcon name="phone" :size="14" />
|
||||||
|
{{ pickingContact ? '…' : 'از مخاطبین' }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="createForm.mobile"
|
v-model="createForm.mobile"
|
||||||
required
|
required
|
||||||
@@ -969,6 +1107,13 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
inputmode="tel"
|
inputmode="tel"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<input
|
||||||
|
ref="contactFileInput"
|
||||||
|
class="hidden-file"
|
||||||
|
type="file"
|
||||||
|
accept=".vcf,text/vcard,text/x-vcard"
|
||||||
|
@change="onContactVcfFile"
|
||||||
|
/>
|
||||||
<div class="create-grid">
|
<div class="create-grid">
|
||||||
<label>
|
<label>
|
||||||
شهر
|
شهر
|
||||||
@@ -1000,6 +1145,57 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="createError" class="error">{{ createError }}</p>
|
<p v-if="createError" class="error">{{ createError }}</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<UiModal nest :open="sourceOpen" title="از مخاطبین" @close="closeSource">
|
||||||
|
<p class="pick-lead">
|
||||||
|
چند مخاطب را از دفترچه یا فایل بگیرید؛ بعد در لیست الفبایی جستجو کنید و یکی را برای پر کردن موبایل انتخاب کنید.
|
||||||
|
</p>
|
||||||
|
<div class="pick-choose">
|
||||||
|
<UiButton variant="primary" block :disabled="pickingContact" @click="pickFromDevice">
|
||||||
|
<UiIcon name="phone" :size="16" />
|
||||||
|
از دفترچه تلفن
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" block :disabled="pickingContact" @click="pickFromVcf">
|
||||||
|
از فایل .vcf
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<UiButton variant="ghost" @click="closeSource">انصراف</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiModal>
|
||||||
|
|
||||||
|
<UiModal nest :open="pickOpen" title="انتخاب مخاطب" @close="closePick">
|
||||||
|
<p class="pick-lead">
|
||||||
|
{{ toFaDigits(pickDrafts.length) }} مخاطب (مرتبشده الفبایی).
|
||||||
|
یکی را انتخاب کنید تا موبایل (و در صورت خالی بودن، نام) پر شود.
|
||||||
|
</p>
|
||||||
|
<div class="pick-search">
|
||||||
|
<UiIcon name="search" :size="15" />
|
||||||
|
<input
|
||||||
|
v-model="pickQuery"
|
||||||
|
type="search"
|
||||||
|
placeholder="جستجو نام یا موبایل…"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul v-if="visiblePickDrafts.length" class="pick-list">
|
||||||
|
<li v-for="row in visiblePickDrafts" :key="row.key">
|
||||||
|
<button type="button" class="pick-row" @click="choosePick(row)">
|
||||||
|
<span class="pick-avatar" aria-hidden="true">{{ row.fullName.slice(0, 1) }}</span>
|
||||||
|
<span class="pick-meta">
|
||||||
|
<strong>{{ row.fullName }}</strong>
|
||||||
|
<small>{{ draftLine(row) }}</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-else class="pick-empty">مخاطبی با این جستجو پیدا نشد.</p>
|
||||||
|
<template #footer>
|
||||||
|
<UiButton variant="ghost" @click="closePick">انصراف</UiButton>
|
||||||
|
<UiButton variant="ghost" @click="contactFileInput?.click()">فایل دیگر</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiModal>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<UiButton variant="ghost" :disabled="createBusy" @click="createOpen = false">
|
<UiButton variant="ghost" :disabled="createBusy" @click="createOpen = false">
|
||||||
انصراف
|
انصراف
|
||||||
@@ -1023,29 +1219,8 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.btn-short {
|
||||||
display: flex;
|
display: none;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.excel-btn {
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-btn {
|
|
||||||
width: var(--control-h, 2.5rem);
|
|
||||||
height: var(--control-h, 2.5rem);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
@@ -1666,6 +1841,140 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-form .label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-contact {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent-deep, var(--accent));
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-contact:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-lead {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-choose {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-search input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--ink);
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
max-height: min(48vh, 360px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
text-align: start;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-row:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-avatar {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-deep, var(--accent));
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-meta strong {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-meta small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-empty {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.create-form input,
|
.create-form input,
|
||||||
.create-form select {
|
.create-form select {
|
||||||
min-height: 2.45rem;
|
min-height: 2.45rem;
|
||||||
@@ -1702,5 +2011,13 @@ onBeforeUnmount(stopImportPoll);
|
|||||||
.search input {
|
.search input {
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.actions) .btn-full {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.actions) .btn-short {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user