Allow empty Tahesab code range to list and import all customers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 10:30:54 +03:30
co-authored by Cursor
parent 3277561e55
commit 38f4b75a78
3 changed files with 101 additions and 34 deletions
+10
View File
@@ -154,6 +154,11 @@ export class ImportBulkFromTahesabDto {
@IsInt()
@Min(1)
toCode?: number;
/** اگر true باشد همهٔ مشتریان با اسکن بازه‌ای خوانده می‌شوند */
@IsOptional()
@IsBoolean()
listAll?: boolean;
}
export class SearchTahesabDto {
@@ -185,6 +190,11 @@ export class SearchTahesabDto {
@IsInt()
@Min(1)
toCode?: number;
/** بازه خالی در UI → همهٔ مشتریان */
@IsOptional()
@IsBoolean()
listAll?: boolean;
}
export class ListAsnadQueryDto {
@@ -15,7 +15,11 @@ import {
MatchPersonDto,
SearchTahesabDto,
} from './dto';
import { normalizeIranMobile, SanadBuilders } from './tahesab-methods';
import { normalizeIranMobile, SanadBuilders, type TahesabApi } from './tahesab-methods';
const LIST_ALL_CHUNK = 2000;
const LIST_ALL_MAX_CODE = 50_000;
const LIST_ALL_EMPTY_STREAK = 3;
function asMoshtariList(raw: unknown): Array<Record<string, unknown>> {
if (!raw) return [];
@@ -312,18 +316,37 @@ export class TahesabPersonLinkService {
return this.linkDto(link);
}
private async listAllMoshtari(api: TahesabApi) {
const merged: Array<Record<string, unknown>> = [];
let emptyStreak = 0;
for (let from = 1; from <= LIST_ALL_MAX_CODE; from += LIST_ALL_CHUNK) {
const to = Math.min(from + LIST_ALL_CHUNK - 1, LIST_ALL_MAX_CODE);
const rows = asMoshtariList(await api.doListMoshtariByCode(from, to));
if (!rows.length) {
emptyStreak += 1;
if (emptyStreak >= LIST_ALL_EMPTY_STREAK) break;
continue;
}
emptyStreak = 0;
merged.push(...rows);
}
return merged;
}
async search(organizationId: string, dto: SearchTahesabDto) {
const { api } = await this.connections.getApi(
organizationId,
dto.connectionId,
);
let raw: unknown = [];
if (dto.fromCode != null && dto.toCode != null) {
if (dto.listAll) {
raw = await this.listAllMoshtari(api);
} else if (dto.fromCode != null && dto.toCode != null) {
if (dto.toCode < dto.fromCode) {
throw new BadRequestException('کد پایان باید بزرگ‌تر یا مساوی کد شروع باشد.');
}
if (dto.toCode - dto.fromCode > 2000) {
throw new BadRequestException('بازهٔ کد حداکثر ۲۰۰۰ عدد است (مثلاً ۱ تا ۲۰۰۰).');
throw new BadRequestException('بازهٔ کد حداکثر ۲۰۰۰ عدد است (مثلاً ۱ تا ۲۰۰۰). برای همه، بازه را خالی بگذارید.');
}
raw = await api.doListMoshtariByCode(dto.fromCode, dto.toCode);
} else if (dto.moshtariCode) {
@@ -335,7 +358,7 @@ export class TahesabPersonLinkService {
raw = await api.doListMoshtariByName(dto.name.trim());
} else {
throw new BadRequestException(
'یکی از موارد لازم است: بازه کد (از–تا)، کد مشتری، موبایل یا نام.',
'یکی از موارد لازم است: بازه کد (از–تا)، کد مشتری، موبایل یا نام. برای همهٔ مشتریان بازه را خالی بگذارید.',
);
}
@@ -481,12 +504,23 @@ export class TahesabPersonLinkService {
async importBulk(organizationId: string, dto: ImportBulkFromTahesabDto) {
let codes = (dto.moshtariCodes ?? []).filter((n) => Number.isFinite(n) && n >= 1);
if (dto.fromCode != null && dto.toCode != null) {
if (dto.listAll) {
const { api } = await this.connections.getApi(
organizationId,
dto.connectionId,
);
const listed = (await this.listAllMoshtari(api))
.map((row) => pickCode(row))
.filter((c): c is number => c != null);
codes = [...new Set([...codes, ...listed])];
} else if (dto.fromCode != null && dto.toCode != null) {
if (dto.toCode < dto.fromCode) {
throw new BadRequestException('کد پایان باید بزرگ‌تر یا مساوی کد شروع باشد.');
}
if (dto.toCode - dto.fromCode > 2000) {
throw new BadRequestException('بازهٔ کد حداکثر ۲۰۰۰ عدد است.');
throw new BadRequestException(
'بازهٔ کد حداکثر ۲۰۰۰ عدد است. برای همه، بازه را خالی بگذارید.',
);
}
const { api } = await this.connections.getApi(
organizationId,
@@ -504,8 +538,11 @@ export class TahesabPersonLinkService {
'کد مشتری یا بازهٔ کد برای وارد کردن مشخص نشده است.',
);
}
if (codes.length > 500) {
throw new BadRequestException('حداکثر ۵۰۰ مشتری در هر بار وارد کنید.');
const maxImport = dto.listAll ? 2000 : 500;
if (codes.length > maxImport) {
throw new BadRequestException(
`حداکثر ${maxImport} مشتری در هر بار وارد کنید (الان ${codes.length}). بازه را کوچک‌تر کنید.`,
);
}
const items: Array<{
+46 -26
View File
@@ -31,8 +31,8 @@ const sync = reactive({
phone: '',
name: '',
moshtariCode: '' as string | number,
fromCode: '1',
toCode: '100',
fromCode: '',
toCode: '',
});
const syncBusy = ref(false);
type SyncResult = {
@@ -234,14 +234,20 @@ async function searchTahesab(mode: 'filter' | 'range' = 'filter') {
connectionId: sync.connectionId,
};
if (mode === 'range') {
const fromCode = Number(sync.fromCode);
const toCode = Number(sync.toCode);
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
error.value = 'بازهٔ کد را درست وارد کنید.';
return;
const fromRaw = String(sync.fromCode ?? '').trim();
const toRaw = String(sync.toCode ?? '').trim();
if (!fromRaw && !toRaw) {
body.listAll = true;
} else {
const fromCode = Number(fromRaw);
const toCode = Number(toRaw);
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
error.value = 'بازهٔ کد را درست وارد کنید، یا هر دو را خالی بگذارید تا همه نمایش داده شوند.';
return;
}
body.fromCode = fromCode;
body.toCode = toCode;
}
body.fromCode = fromCode;
body.toCode = toCode;
} else if (sync.moshtariCode !== '' && sync.moshtariCode != null) {
body.moshtariCode = Number(sync.moshtariCode);
} else if (sync.phone.trim()) {
@@ -315,21 +321,34 @@ async function importBulk(mode: 'results' | 'range') {
}
body.moshtariCodes = codes;
} else {
const fromCode = Number(sync.fromCode);
const toCode = Number(sync.toCode);
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
error.value = 'بازهٔ کد را درست وارد کنید.';
return;
const fromRaw = String(sync.fromCode ?? '').trim();
const toRaw = String(sync.toCode ?? '').trim();
if (!fromRaw && !toRaw) {
if (
!confirm(
'همهٔ مشتریان ته‌حساب وارد منتو می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ممکن است کمی طول بکشد. ادامه؟',
)
) {
return;
}
body.listAll = true;
} else {
const fromCode = Number(fromRaw);
const toCode = Number(toRaw);
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
error.value = 'بازهٔ کد را درست وارد کنید، یا هر دو را خالی بگذارید تا همه وارد شوند.';
return;
}
if (
!confirm(
`همهٔ مشتریان ته‌حساب با کد ${fromCode} تا ${toCode} وارد منتو می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ادامه؟`,
)
) {
return;
}
body.fromCode = fromCode;
body.toCode = toCode;
}
if (
!confirm(
`همهٔ مشتریان ته‌حساب با کد ${fromCode} تا ${toCode} وارد منتو می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ادامه؟`,
)
) {
return;
}
body.fromCode = fromCode;
body.toCode = toCode;
}
try {
syncBusy.value = true;
@@ -483,11 +502,12 @@ onMounted(load);
<h3 class="subhead">۱) وارد کردن دسته‌ای با بازهٔ کد مشتری</h3>
<p class="muted small">
معمولاً کدها از ۱ شروع می‌شوند. بازه را مطابق دیتابیس ته‌حساب بگذارید (حداکثر ۲۰۰۰ کد در هر بار).
اگر «از» و «تا» را خالی بگذارید و نمایش بازه را بزنید، همهٔ مشتریان ته‌حساب لیست می‌شوند.
برای محدود کردن، بازه بگذارید (حداکثر ۲۰۰۰ کد در هر درخواست محدود).
</p>
<div class="sync-grid range">
<input v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد" />
<input v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد" />
<input v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (خالی = همه)" />
<input v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (خالی = همه)" />
</div>
<div class="sync-actions">
<button