feat: customer groups for people and companies with tahesab group sync and bulk register
Multiple groups per contact, legacy group text migrated, excel import/export, contacts filter and column, richer create-in-tahesab, match-first bulk export
This commit is contained in:
@@ -496,6 +496,128 @@ async function importBulk() {
|
||||
}
|
||||
}
|
||||
|
||||
/** همهٔ بازههای کد خوانده میشود و گروه تهحساب روی مشتریهای لینکشده نشانده میشود */
|
||||
async function syncGroups() {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
if (!sync.connectionId) {
|
||||
error.value = 'ابتدا اتصال را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
syncBusy.value = true;
|
||||
importOverlay.open = true;
|
||||
importOverlay.progress = null;
|
||||
const totals = { rows: 0, linked: 0, updated: 0 };
|
||||
try {
|
||||
let page = 1;
|
||||
let emptyStreak = 0;
|
||||
while (emptyStreak < LIST_ALL_EMPTY_STOP) {
|
||||
importOverlay.message = `در حال همگامسازی گروهها… ${toFaDigits(totals.rows)} مشتری بررسی شد · ${toFaDigits(totals.updated)} تغییر`;
|
||||
const { data } = await api.post<{ rows: number; linked: number; updated: number }>(
|
||||
'/integrations/tahesab/sync/groups',
|
||||
{ connectionId: sync.connectionId, page, pageSize: LIST_ALL_CODE_PAGE },
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
totals.rows += data.rows;
|
||||
totals.linked += data.linked;
|
||||
totals.updated += data.updated;
|
||||
emptyStreak = data.rows ? 0 : emptyStreak + 1;
|
||||
page += 1;
|
||||
}
|
||||
ok.value = `گروهها همگام شد: ${toFaDigits(totals.linked)} مشتری لینکشده بررسی شد · گروه ${toFaDigits(totals.updated)} نفر بهروز شد.`;
|
||||
} catch (e) {
|
||||
error.value = apiErr(e, 'همگامسازی گروهها ناموفق بود');
|
||||
} finally {
|
||||
importOverlay.open = false;
|
||||
syncBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
type ExportItem = {
|
||||
personId: string;
|
||||
name: string;
|
||||
status: 'matched' | 'created' | 'ambiguous' | 'skipped' | 'error';
|
||||
moshtariCode?: number;
|
||||
error?: string;
|
||||
};
|
||||
const EXPORT_CHUNK = 10;
|
||||
const exportIssues = ref<ExportItem[]>([]);
|
||||
|
||||
/** شخصهای لینکنشده: اول تطبیق، بعد ساخت در تهحساب (بدون ساخت تکراری) */
|
||||
async function exportUnlinked() {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
exportIssues.value = [];
|
||||
if (!sync.connectionId) {
|
||||
error.value = 'ابتدا اتصال را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
syncBusy.value = true;
|
||||
try {
|
||||
const { data: unlinked } = await api.get<{ ids: string[]; total: number }>(
|
||||
'/integrations/tahesab/sync/unlinked-people',
|
||||
{ params: { connectionId: sync.connectionId } },
|
||||
);
|
||||
if (!unlinked.total) {
|
||||
ok.value = 'همهٔ اشخاص به تهحساب لینک هستند.';
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`${toFaDigits(unlinked.total)} شخص به تهحساب لینک نیستند.\n` +
|
||||
'برای هر نفر اول با موبایل/نام در تهحساب جستجو میشود: اگر دقیقاً یک نفر پیدا شد فقط لینک میشود، ' +
|
||||
'اگر چند نفر پیدا شد رد میشود تا دستی بررسی کنید، و اگر پیدا نشد در تهحساب ساخته میشود. ادامه؟',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
importOverlay.open = true;
|
||||
importOverlay.progress = 0;
|
||||
const totals = { matched: 0, created: 0, ambiguous: 0, skipped: 0, failed: 0 };
|
||||
const ids = unlinked.ids;
|
||||
for (let i = 0; i < ids.length; i += EXPORT_CHUNK) {
|
||||
const chunk = ids.slice(i, i + EXPORT_CHUNK);
|
||||
importOverlay.message = `در حال ثبت در تهحساب… ${toFaDigits(Math.min(i + chunk.length, ids.length))} از ${toFaDigits(ids.length)}`;
|
||||
try {
|
||||
const { data } = await api.post<typeof totals & { items: ExportItem[] }>(
|
||||
'/integrations/tahesab/sync/export-people',
|
||||
{ connectionId: sync.connectionId, personIds: chunk },
|
||||
{ timeout: 180_000 },
|
||||
);
|
||||
totals.matched += data.matched;
|
||||
totals.created += data.created;
|
||||
totals.ambiguous += data.ambiguous;
|
||||
totals.skipped += data.skipped;
|
||||
totals.failed += data.failed;
|
||||
exportIssues.value.push(
|
||||
...data.items.filter((it) => it.status === 'ambiguous' || it.status === 'error'),
|
||||
);
|
||||
} catch (e) {
|
||||
totals.failed += chunk.length;
|
||||
exportIssues.value.push(
|
||||
...chunk.map((personId) => ({
|
||||
personId,
|
||||
name: '',
|
||||
status: 'error' as const,
|
||||
error: apiErr(e, 'خطا در ارسال'),
|
||||
})),
|
||||
);
|
||||
}
|
||||
importOverlay.progress = ((i + chunk.length) / ids.length) * 100;
|
||||
}
|
||||
ok.value =
|
||||
`ثبت در تهحساب تمام شد: لینک با مشتری موجود ${toFaDigits(totals.matched)} · ` +
|
||||
`ساختهشده ${toFaDigits(totals.created)} · چند نتیجه (نیاز به بررسی) ${toFaDigits(totals.ambiguous)} · ` +
|
||||
`ناموفق ${toFaDigits(totals.failed)}`;
|
||||
} catch (e) {
|
||||
error.value = apiErr(e, 'ثبت گروهی در تهحساب ناموفق بود');
|
||||
} finally {
|
||||
importOverlay.open = false;
|
||||
importOverlay.progress = null;
|
||||
syncBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function searchProducts() {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
@@ -807,6 +929,43 @@ onMounted(load);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sync-sub-section">
|
||||
<h4 class="sync-subhead">۳) گروهها و ثبت مشتریهای منتکت در تهحساب</h4>
|
||||
<p class="sync-sub-desc">
|
||||
«همگامسازی گروهها» گروه هر مشتری لینکشده را از تهحساب میخواند؛ فقط گروهِ آمده از تهحساب عوض
|
||||
میشود و گروههایی که دستی دادهاید میمانند.
|
||||
«ثبت لینکنشدهها» برای هر شخصی که به تهحساب لینک نیست اول تطبیق (موبایل، بعد نام) را امتحان
|
||||
میکند تا مشتری تکراری ساخته نشود؛ اگر پیدا نشد با گروه، آدرس، کد ملی و تاریخ تولد ساخته میشود.
|
||||
</p>
|
||||
<div class="sync-buttons">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="syncGroups"
|
||||
>
|
||||
همگامسازی گروهها از تهحساب
|
||||
</UiButton>
|
||||
<UiButton :disabled="!sync.connectionId || syncBusy" @click="exportUnlinked">
|
||||
ثبت مشتریهای لینکنشده در تهحساب
|
||||
</UiButton>
|
||||
</div>
|
||||
<div v-if="exportIssues.length" class="export-issues">
|
||||
<p class="sync-sub-desc">
|
||||
این اشخاص ثبت نشدند ({{ toFaDigits(exportIssues.length) }}). از پرونده، بخش «تهحساب و دارایی»
|
||||
دستی لینک یا ثبت کنید:
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="it in exportIssues" :key="it.personId">
|
||||
<RouterLink :to="{ name: 'person-detail', params: { id: it.personId } }">
|
||||
{{ it.name || 'بدون نام' }}
|
||||
</RouterLink>
|
||||
—
|
||||
{{ it.status === 'ambiguous' ? 'چند مشتری مشابه در تهحساب پیدا شد' : it.error || 'خطا' }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="syncResults.length" class="sync-results-box">
|
||||
<h4 class="sync-results-title">
|
||||
نتایج جستجو ({{ syncVisibleResults.length }}<template v-if="syncOnlyNew"> از {{ syncResults.length }}</template>)
|
||||
@@ -1176,6 +1335,14 @@ onMounted(load);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.export-issues ul {
|
||||
margin: var(--sp-2) 0 0;
|
||||
padding-inline-start: 1.2rem;
|
||||
font-size: var(--fs-xs);
|
||||
max-height: 14rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sync-table-pagination {
|
||||
margin-top: var(--sp-3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user