Edit from phone contacts
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared';
|
||||
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
import {
|
||||
filterPhoneContacts,
|
||||
isContactPickerSupported,
|
||||
parseVCardFile,
|
||||
pickOnePhoneContact,
|
||||
sortPhoneContacts,
|
||||
type PhoneContactDraft,
|
||||
} from '@/lib/phone-contacts';
|
||||
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
UiCard,
|
||||
UiField,
|
||||
UiInput,
|
||||
UiModal,
|
||||
UiPageHeader,
|
||||
UiSelect,
|
||||
UiTextarea,
|
||||
@@ -65,6 +69,13 @@ const loyaltyTierName = ref('');
|
||||
const contactPickerSupported = isContactPickerSupported();
|
||||
const pickingContact = ref(false);
|
||||
const contactFileInput = ref<HTMLInputElement | null>(null);
|
||||
const pickOpen = ref(false);
|
||||
const pickDrafts = ref<PhoneContactDraft[]>([]);
|
||||
const pickQuery = ref('');
|
||||
|
||||
const visiblePickDrafts = computed(() =>
|
||||
filterPhoneContacts(pickDrafts.value, pickQuery.value),
|
||||
);
|
||||
|
||||
const backTo = computed(() =>
|
||||
isNew.value ? { name: 'contacts' as const } : { name: 'person-detail' as const, params: { id: id.value } },
|
||||
@@ -91,6 +102,29 @@ function applyContactDraft(draft: PhoneContactDraft) {
|
||||
}
|
||||
}
|
||||
|
||||
function draftLine(row: PhoneContactDraft) {
|
||||
return [row.mobile, row.phone, row.email].filter(Boolean).join(' · ') || 'بدون شماره/ایمیل';
|
||||
}
|
||||
|
||||
function closePick() {
|
||||
pickOpen.value = false;
|
||||
pickDrafts.value = [];
|
||||
pickQuery.value = '';
|
||||
}
|
||||
|
||||
function choosePick(row: PhoneContactDraft) {
|
||||
applyContactDraft(row);
|
||||
closePick();
|
||||
}
|
||||
|
||||
async function openPickList(rows: PhoneContactDraft[]) {
|
||||
pickDrafts.value = sortPhoneContacts(rows);
|
||||
pickQuery.value = '';
|
||||
pickOpen.value = false;
|
||||
await nextTick();
|
||||
pickOpen.value = true;
|
||||
}
|
||||
|
||||
async function fillFromPhoneContact() {
|
||||
if (pickingContact.value || saving.value) return;
|
||||
pickingContact.value = true;
|
||||
@@ -126,12 +160,11 @@ async function onContactVcfFile(ev: Event) {
|
||||
error.value = '';
|
||||
try {
|
||||
const rows = await parseVCardFile(file);
|
||||
const draft = rows[0];
|
||||
if (!draft) {
|
||||
if (!rows.length) {
|
||||
error.value = 'مخاطبی در فایل پیدا نشد.';
|
||||
return;
|
||||
}
|
||||
applyContactDraft(draft);
|
||||
await openPickList(rows);
|
||||
} catch {
|
||||
error.value = 'خواندن فایل مخاطب (.vcf) ناموفق بود.';
|
||||
} finally {
|
||||
@@ -306,6 +339,38 @@ onMounted(load);
|
||||
@change="onContactVcfFile"
|
||||
/>
|
||||
|
||||
<UiModal :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>
|
||||
|
||||
<UiField label="تلفن" for-id="phone">
|
||||
<UiInput id="phone" v-model="phone" type="tel" />
|
||||
</UiField>
|
||||
@@ -489,6 +554,102 @@ onMounted(load);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pick-lead {
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-sm);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.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: var(--radius);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.pick-search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.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: var(--radius);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.pick-row:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.pick-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent-deep);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: var(--fw-bold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pick-meta {
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pick-meta strong {
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
.pick-meta small {
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-xs);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pick-empty {
|
||||
margin: 0.75rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
.mob-only {
|
||||
display: none !important;
|
||||
|
||||
Reference in New Issue
Block a user