fix: stop Tahesab imports from stealing links between shared-mobile customers
List all Tahesab customers in one paginated table with a page-size picker instead of 2000-code pages.
This commit is contained in:
@@ -548,12 +548,15 @@ export class TahesabPersonLinkService {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// اگر موبایل تکراری است → همان شخص؛ نام قبلی حفظ و نام تهحساب جایگزین
|
||||
// اگر موبایل تکراری است → همان شخص؛ نام قبلی حفظ و نام تهحساب جایگزین.
|
||||
// شخصی که روی همین اتصال به کد دیگری لینک است کنار گذاشته میشود؛
|
||||
// وگرنه upsert لینک، کد قبلی را جابهجا و آن مشتری را دوباره «واردنشده» میکند.
|
||||
const byMobile = mobile
|
||||
? await this.prisma.person.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
OR: [{ mobile }, { phone: mobile }],
|
||||
accountingLinks: { none: { connectionId: dto.connectionId } },
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -66,25 +66,13 @@ type SyncResult = {
|
||||
personName?: string | null;
|
||||
};
|
||||
const syncResults = ref<SyncResult[]>([]);
|
||||
/** حالت آخرین جستجو — برای پیجینیت بازهٔ کد */
|
||||
const syncListMode = ref<'none' | 'listAll' | 'range' | 'filter'>('none');
|
||||
const syncListPage = ref(1);
|
||||
const syncListMeta = reactive({
|
||||
fromCode: 0,
|
||||
toCode: 0,
|
||||
hasMore: false,
|
||||
pageSize: 2000,
|
||||
});
|
||||
const SYNC_TABLE_PAGE_SIZE = 50;
|
||||
/** هر درخواست listAll یک بازهٔ ۲۰۰۰ کدی است؛ بعد از دو بازهٔ خالی پشتسرهم، خواندن تمام میشود */
|
||||
const LIST_ALL_CODE_PAGE = 2000;
|
||||
const LIST_ALL_EMPTY_STOP = 2;
|
||||
const SYNC_TABLE_PAGE_SIZES = [10, 20, 50, 100];
|
||||
const syncTablePageSize = ref(50);
|
||||
const syncTablePage = ref(1);
|
||||
|
||||
const syncCodeTotalPages = computed(() => {
|
||||
if (syncListMode.value !== 'listAll') return 1;
|
||||
// اگر صفحه فعلی نتیجه دارد، صفحهٔ بعد هم قابل امتحان است
|
||||
return syncListMeta.hasMore
|
||||
? syncListPage.value + 1
|
||||
: Math.max(1, syncListPage.value);
|
||||
});
|
||||
const syncOnlyNew = ref(false);
|
||||
const syncVisibleResults = computed(() =>
|
||||
syncOnlyNew.value
|
||||
@@ -92,15 +80,18 @@ const syncVisibleResults = computed(() =>
|
||||
: syncResults.value,
|
||||
);
|
||||
const syncTableTotalPages = computed(() =>
|
||||
Math.max(1, Math.ceil(syncVisibleResults.value.length / SYNC_TABLE_PAGE_SIZE)),
|
||||
Math.max(1, Math.ceil(syncVisibleResults.value.length / syncTablePageSize.value)),
|
||||
);
|
||||
const syncPagedResults = computed(() => {
|
||||
const start = (syncTablePage.value - 1) * SYNC_TABLE_PAGE_SIZE;
|
||||
return syncVisibleResults.value.slice(start, start + SYNC_TABLE_PAGE_SIZE);
|
||||
const start = (syncTablePage.value - 1) * syncTablePageSize.value;
|
||||
return syncVisibleResults.value.slice(start, start + syncTablePageSize.value);
|
||||
});
|
||||
watch(syncOnlyNew, () => {
|
||||
watch([syncOnlyNew, syncTablePageSize], () => {
|
||||
syncTablePage.value = 1;
|
||||
});
|
||||
watch(syncTableTotalPages, (total) => {
|
||||
if (syncTablePage.value > total) syncTablePage.value = total;
|
||||
});
|
||||
|
||||
const productSync = reactive({
|
||||
connectionId: '',
|
||||
@@ -301,7 +292,43 @@ async function removeConn(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function searchTahesab(mode: 'range' | 'filter', codePage?: number) {
|
||||
type SyncSearchResponse = { results: SyncResult[] };
|
||||
|
||||
async function fetchAllCustomers(): Promise<SyncResult[]> {
|
||||
const all: SyncResult[] = [];
|
||||
let page = 1;
|
||||
let emptyStreak = 0;
|
||||
importOverlay.open = true;
|
||||
importOverlay.progress = null;
|
||||
try {
|
||||
while (emptyStreak < LIST_ALL_EMPTY_STOP) {
|
||||
importOverlay.message = `در حال خواندن مشتریان از تهحساب… ${toFaDigits(all.length)} مشتری`;
|
||||
const { data } = await api.post<SyncSearchResponse>(
|
||||
'/integrations/tahesab/sync/search',
|
||||
{
|
||||
connectionId: sync.connectionId,
|
||||
listAll: true,
|
||||
page,
|
||||
pageSize: LIST_ALL_CODE_PAGE,
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
const rows = data.results ?? [];
|
||||
if (rows.length) {
|
||||
all.push(...rows);
|
||||
emptyStreak = 0;
|
||||
} else {
|
||||
emptyStreak += 1;
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
} finally {
|
||||
importOverlay.open = false;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
async function searchTahesab(mode: 'range' | 'filter') {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
if (!sync.connectionId) {
|
||||
@@ -311,17 +338,12 @@ async function searchTahesab(mode: 'range' | 'filter', codePage?: number) {
|
||||
syncBusy.value = true;
|
||||
try {
|
||||
const body: Record<string, unknown> = { connectionId: sync.connectionId };
|
||||
let listAll = false;
|
||||
if (mode === 'range') {
|
||||
const fromRaw = String(sync.fromCode ?? '').trim();
|
||||
const toRaw = String(sync.toCode ?? '').trim();
|
||||
if (!fromRaw && !toRaw) {
|
||||
// همهٔ مشتریان — صفحهبندی بازهٔ کد (هر صفحه ۲۰۰۰ کد؛ بدون سقف ۵۰۰۰)
|
||||
const page = Math.max(1, codePage ?? syncListPage.value);
|
||||
body.listAll = true;
|
||||
body.page = page;
|
||||
body.pageSize = 2000;
|
||||
syncListPage.value = page;
|
||||
syncListMode.value = 'listAll';
|
||||
listAll = true;
|
||||
} else {
|
||||
const fromCode = fromRaw ? Number(fromRaw) : 1;
|
||||
const toCode = toRaw ? Number(toRaw) : fromCode + 1999;
|
||||
@@ -335,59 +357,38 @@ async function searchTahesab(mode: 'range' | 'filter', codePage?: number) {
|
||||
}
|
||||
body.fromCode = fromCode;
|
||||
body.toCode = toCode;
|
||||
syncListMode.value = 'range';
|
||||
syncListMeta.hasMore = false;
|
||||
}
|
||||
} else if (sync.moshtariCode !== '' && sync.moshtariCode != null) {
|
||||
body.moshtariCode = Number(sync.moshtariCode);
|
||||
syncListMode.value = 'filter';
|
||||
syncListMeta.hasMore = false;
|
||||
} else if (sync.phone.trim()) {
|
||||
body.phone = sync.phone.trim();
|
||||
syncListMode.value = 'filter';
|
||||
syncListMeta.hasMore = false;
|
||||
} else if (sync.name.trim()) {
|
||||
body.name = sync.name.trim();
|
||||
syncListMode.value = 'filter';
|
||||
syncListMeta.hasMore = false;
|
||||
} else {
|
||||
error.value = 'موبایل، نام، کد مشتری یا بازهٔ کد را وارد کنید.';
|
||||
return;
|
||||
}
|
||||
const { data } = await api.post<{
|
||||
results: SyncResult[];
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
fromCode?: number;
|
||||
toCode?: number;
|
||||
hasMore?: boolean;
|
||||
note?: string;
|
||||
}>(
|
||||
'/integrations/tahesab/sync/search',
|
||||
body,
|
||||
{ timeout: body.listAll ? 90_000 : 60_000 },
|
||||
);
|
||||
syncResults.value = data.results ?? [];
|
||||
syncTablePage.value = 1;
|
||||
if (data.page != null) {
|
||||
syncListPage.value = data.page;
|
||||
syncListMeta.fromCode = data.fromCode ?? 0;
|
||||
syncListMeta.toCode = data.toCode ?? 0;
|
||||
syncListMeta.hasMore = Boolean(data.hasMore);
|
||||
syncListMeta.pageSize = data.pageSize ?? 2000;
|
||||
|
||||
if (listAll) {
|
||||
syncResults.value = await fetchAllCustomers();
|
||||
} else {
|
||||
const { data } = await api.post<SyncSearchResponse>(
|
||||
'/integrations/tahesab/sync/search',
|
||||
body,
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
syncResults.value = data.results ?? [];
|
||||
}
|
||||
syncTablePage.value = 1;
|
||||
|
||||
if (!syncResults.value.length) {
|
||||
ok.value =
|
||||
data.note ??
|
||||
(syncListMode.value === 'listAll' && syncListPage.value > 1
|
||||
? 'در این صفحه مشتریای نیست. صفحه قبل را ببینید یا بازه دستی بزنید.'
|
||||
: 'نتیجهای یافت نشد.');
|
||||
ok.value = 'نتیجهای یافت نشد.';
|
||||
} else {
|
||||
const linked = syncResults.value.filter((r) => r.alreadyLinked).length;
|
||||
const base =
|
||||
`${syncResults.value.length} مشتری پیدا شد` +
|
||||
(linked ? ` (${linked} قبلاً لینک شده)` : '');
|
||||
ok.value = data.note ? `${base} — ${data.note}` : base;
|
||||
const pending = syncResults.value.length - linked;
|
||||
ok.value =
|
||||
`${toFaDigits(syncResults.value.length)} مشتری پیدا شد` +
|
||||
` · ${toFaDigits(linked)} قبلاً وارد شده · ${toFaDigits(pending)} واردنشده`;
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = apiErr(e, 'جستجو ناموفق');
|
||||
@@ -396,10 +397,6 @@ async function searchTahesab(mode: 'range' | 'filter', codePage?: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function onSyncCodePageChange(page: number) {
|
||||
void searchTahesab('range', page);
|
||||
}
|
||||
|
||||
function onSyncTablePageChange(page: number) {
|
||||
syncTablePage.value = page;
|
||||
}
|
||||
@@ -426,72 +423,24 @@ async function importOne(moshtariCode: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function importBulk(mode: 'results' | 'range') {
|
||||
async function importBulk() {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
if (!sync.connectionId) {
|
||||
error.value = 'ابتدا اتصال را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
const body: Record<string, unknown> = { connectionId: sync.connectionId };
|
||||
if (mode === 'results') {
|
||||
const codes = pendingImportCodes();
|
||||
if (!codes.length) {
|
||||
error.value = 'مشتری جدیدی در نتایج برای وارد کردن نیست (همه لینک شدهاند یا لیست خالی است).';
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`${codes.length} مشتری جدید از تهحساب بهعنوان شخص در ${brand.nameFa} ساخته و لینک میشوند. ادامه؟`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
body.moshtariCodes = codes;
|
||||
} else {
|
||||
const fromRaw = String(sync.fromCode ?? '').trim();
|
||||
const toRaw = String(sync.toCode ?? '').trim();
|
||||
if (!fromRaw && !toRaw) {
|
||||
const page = syncListMode.value === 'listAll' ? syncListPage.value : 1;
|
||||
const from =
|
||||
syncListMode.value === 'listAll' && syncListMeta.fromCode
|
||||
? syncListMeta.fromCode
|
||||
: (page - 1) * 2000 + 1;
|
||||
const to =
|
||||
syncListMode.value === 'listAll' && syncListMeta.toCode
|
||||
? syncListMeta.toCode
|
||||
: from + 1999;
|
||||
if (
|
||||
!confirm(
|
||||
`مشتریان صفحهٔ ${page} (کد ${from} تا ${to}) از تهحساب بهعنوان شخص وارد ${brand.nameFa} میشوند. برای صفحات بعدی، پیجینیت را بزنید و دوباره وارد کنید. ادامه؟`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
body.listAll = true;
|
||||
body.page = page;
|
||||
body.pageSize = 2000;
|
||||
} else {
|
||||
const fromCode = fromRaw ? Number(fromRaw) : 1;
|
||||
const toCode = toRaw ? Number(toRaw) : fromCode + 1999;
|
||||
if (Number.isNaN(fromCode) || Number.isNaN(toCode) || fromCode < 1 || toCode < fromCode) {
|
||||
error.value = 'بازه نامعتبر است.';
|
||||
return;
|
||||
}
|
||||
if (toCode - fromCode > 2000) {
|
||||
error.value = 'حداکثر طول بازه در هر درخواست ۲۰۰۰ کد است.';
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`مشتریان با کد ${fromCode} تا ${toCode} از تهحساب وارد ${brand.nameFa} میشوند. ادامه؟`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
body.fromCode = fromCode;
|
||||
body.toCode = toCode;
|
||||
}
|
||||
const codes = pendingImportCodes();
|
||||
if (!codes.length) {
|
||||
error.value = 'مشتری جدیدی در نتایج برای وارد کردن نیست (همه وارد شدهاند یا لیست خالی است).';
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`${codes.length} مشتری جدید از تهحساب بهعنوان شخص در ${brand.nameFa} ساخته و لینک میشوند. ادامه؟`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
syncBusy.value = true;
|
||||
importOverlay.open = true;
|
||||
@@ -500,22 +449,6 @@ async function importBulk(mode: 'results' | 'range') {
|
||||
const totals = { imported: 0, skipped: 0, failed: 0 };
|
||||
let firstError = '';
|
||||
try {
|
||||
let codes: number[];
|
||||
if (Array.isArray(body.moshtariCodes)) {
|
||||
codes = body.moshtariCodes as number[];
|
||||
} else {
|
||||
const { data } = await api.post<{ results: SyncResult[] }>(
|
||||
'/integrations/tahesab/sync/search',
|
||||
body,
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
const found = data.results ?? [];
|
||||
totals.skipped += found.filter((r) => r.alreadyLinked).length;
|
||||
codes = found
|
||||
.filter((r) => r.moshtariCode != null && !r.alreadyLinked)
|
||||
.map((r) => r.moshtariCode as number);
|
||||
}
|
||||
|
||||
const total = codes.length;
|
||||
for (let i = 0; i < total; i += IMPORT_CHUNK) {
|
||||
const chunk = codes.slice(i, i + IMPORT_CHUNK);
|
||||
@@ -682,7 +615,7 @@ onMounted(load);
|
||||
class="crm-alert crm-alert-warning"
|
||||
>
|
||||
اتصال شما بهخاطر گواهی SSL خودامضا قطع است. روی همان کارت اتصال،
|
||||
<strong>فعالسازی SSL خودامضا</strong> را بزنید؛ بعد دوباره «نمایش بازه» را امتحان کنید.
|
||||
<strong>فعالسازی SSL خودامضا</strong> را بزنید؛ بعد دوباره «نمایش مشتریان» را امتحان کنید.
|
||||
</div>
|
||||
|
||||
<UiCard v-if="!loading && items.length" title="اتصالات تعریفشده" class="section-card">
|
||||
@@ -823,45 +756,31 @@ onMounted(load);
|
||||
<div class="sync-sub-section">
|
||||
<h4 class="sync-subhead">۱) وارد کردن دستهای با بازه کد مشتری</h4>
|
||||
<p class="sync-sub-desc">
|
||||
بازهٔ دستی حداکثر ۲۰۰۰ کد است. اگر «از» و «تا» خالی باشد، مشتریان صفحهبهصفحه (هر صفحه ۲۰۰۰ کد) خوانده میشوند — بدون سقف ۵۰۰۰.
|
||||
اگر «از» و «تا» خالی باشد، همهٔ مشتریان تهحساب خوانده و در جدول زیر نمایش داده میشوند. بازهٔ دستی حداکثر ۲۰۰۰ کد است.
|
||||
</p>
|
||||
<div class="crm-form-grid-2">
|
||||
<UiInput v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (مثلاً ۱)" />
|
||||
<UiInput v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (مثلاً ۵۰۰)" />
|
||||
<UiInput v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (اختیاری)" />
|
||||
<UiInput v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (اختیاری)" />
|
||||
</div>
|
||||
<div class="sync-buttons">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="searchTahesab('range', 1)"
|
||||
@click="searchTahesab('range')"
|
||||
>
|
||||
نمایش بازه / همه
|
||||
نمایش مشتریان
|
||||
</UiButton>
|
||||
<UiButton
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="importBulk('range')"
|
||||
:disabled="!sync.connectionId || syncBusy || !pendingImportCodes().length"
|
||||
@click="importBulk"
|
||||
>
|
||||
وارد کردن این صفحه / بازه
|
||||
وارد کردن همهٔ واردنشدهها ({{ toFaDigits(pendingImportCodes().length) }})
|
||||
</UiButton>
|
||||
</div>
|
||||
<label class="custom-check">
|
||||
<input v-model="syncOnlyNew" type="checkbox" />
|
||||
<span>فقط مشتریانی که وارد نشدهاند</span>
|
||||
</label>
|
||||
<UiPagination
|
||||
v-if="syncListMode === 'listAll' && (syncResults.length || syncListPage > 1)"
|
||||
:page="syncListPage"
|
||||
:total-pages="syncCodeTotalPages"
|
||||
class="sync-code-pagination"
|
||||
@update:page="onSyncCodePageChange"
|
||||
/>
|
||||
<p
|
||||
v-if="syncListMode === 'listAll' && syncListMeta.fromCode"
|
||||
class="sync-sub-desc"
|
||||
>
|
||||
صفحه {{ syncListPage }} — کد {{ syncListMeta.fromCode }} تا {{ syncListMeta.toCode }}
|
||||
<span v-if="syncListMeta.hasMore"> · صفحه بعد برای کدهای بالاتر</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sync-sub-section">
|
||||
@@ -881,9 +800,9 @@ onMounted(load);
|
||||
</UiButton>
|
||||
<UiButton
|
||||
:disabled="!sync.connectionId || syncBusy || !pendingImportCodes().length"
|
||||
@click="importBulk('results')"
|
||||
@click="importBulk"
|
||||
>
|
||||
وارد کردن نتایج جدید ({{ pendingImportCodes().length }})
|
||||
وارد کردن نتایج جدید ({{ toFaDigits(pendingImportCodes().length) }})
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -899,7 +818,7 @@ onMounted(load);
|
||||
<table
|
||||
v-table-cols="'tahesab.customers'"
|
||||
class="crm-table"
|
||||
:style="{ '--row-start': (syncTablePage - 1) * SYNC_TABLE_PAGE_SIZE }"
|
||||
:style="{ '--row-start': (syncTablePage - 1) * syncTablePageSize }"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -947,13 +866,15 @@ onMounted(load);
|
||||
</table>
|
||||
</div>
|
||||
<UiPagination
|
||||
v-if="syncTableTotalPages > 1"
|
||||
v-if="syncVisibleResults.length > SYNC_TABLE_PAGE_SIZES[0]"
|
||||
:page="syncTablePage"
|
||||
:total-pages="syncTableTotalPages"
|
||||
:total-items="syncVisibleResults.length"
|
||||
:page-size="SYNC_TABLE_PAGE_SIZE"
|
||||
:page-size="syncTablePageSize"
|
||||
:page-size-options="SYNC_TABLE_PAGE_SIZES"
|
||||
class="sync-table-pagination"
|
||||
@update:page="onSyncTablePageChange"
|
||||
@update:page-size="syncTablePageSize = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1255,7 +1176,6 @@ onMounted(load);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sync-code-pagination,
|
||||
.sync-table-pagination {
|
||||
margin-top: var(--sp-3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user