diff --git a/apps/api/src/integrations/tahesab/dto.ts b/apps/api/src/integrations/tahesab/dto.ts index 1ddac86..7c2dd91 100644 --- a/apps/api/src/integrations/tahesab/dto.ts +++ b/apps/api/src/integrations/tahesab/dto.ts @@ -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 { diff --git a/apps/api/src/integrations/tahesab/person-link.service.ts b/apps/api/src/integrations/tahesab/person-link.service.ts index 3e766d7..f11dcaa 100644 --- a/apps/api/src/integrations/tahesab/person-link.service.ts +++ b/apps/api/src/integrations/tahesab/person-link.service.ts @@ -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> { if (!raw) return []; @@ -312,18 +316,37 @@ export class TahesabPersonLinkService { return this.linkDto(link); } + private async listAllMoshtari(api: TahesabApi) { + const merged: Array> = []; + 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<{ diff --git a/apps/web/src/views/TahesabSettingsView.vue b/apps/web/src/views/TahesabSettingsView.vue index e5f0290..87eaec7 100644 --- a/apps/web/src/views/TahesabSettingsView.vue +++ b/apps/web/src/views/TahesabSettingsView.vue @@ -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);

۱) وارد کردن دسته‌ای با بازهٔ کد مشتری

- معمولاً کدها از ۱ شروع می‌شوند. بازه را مطابق دیتابیس ته‌حساب بگذارید (حداکثر ۲۰۰۰ کد در هر بار). + اگر «از» و «تا» را خالی بگذارید و نمایش بازه را بزنید، همهٔ مشتریان ته‌حساب لیست می‌شوند. + برای محدود کردن، بازه بگذارید (حداکثر ۲۰۰۰ کد در هر درخواست محدود).

- - + +