Import Tahesab customers in batches with progress overlay and prefetched rows
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -465,6 +465,7 @@ export class TahesabPersonLinkService {
|
||||
async importFromTahesab(
|
||||
organizationId: string,
|
||||
dto: ImportFromTahesabDto,
|
||||
prefetched?: { api: TahesabApi; row?: Record<string, unknown> },
|
||||
) {
|
||||
const clash = await this.prisma.personAccountingLink.findFirst({
|
||||
where: {
|
||||
@@ -479,16 +480,19 @@ export class TahesabPersonLinkService {
|
||||
);
|
||||
}
|
||||
|
||||
const { api } = await this.connections.getApi(
|
||||
organizationId,
|
||||
dto.connectionId,
|
||||
);
|
||||
const raw = await api.doListMoshtariByCode(
|
||||
dto.moshtariCode,
|
||||
dto.moshtariCode,
|
||||
);
|
||||
const rows = asMoshtariList(raw);
|
||||
const row = rows[0] ?? {};
|
||||
let row: Record<string, unknown>;
|
||||
if (prefetched?.row) {
|
||||
row = prefetched.row;
|
||||
} else {
|
||||
const api =
|
||||
prefetched?.api ??
|
||||
(await this.connections.getApi(organizationId, dto.connectionId)).api;
|
||||
const raw = await api.doListMoshtariByCode(
|
||||
dto.moshtariCode,
|
||||
dto.moshtariCode,
|
||||
);
|
||||
row = (asMoshtariList(raw)[0] ?? {}) as Record<string, unknown>;
|
||||
}
|
||||
const name = pickName(row) || `مشتری تهحساب ${dto.moshtariCode}`;
|
||||
const tel = pickTel(row);
|
||||
const parts = name.trim().split(/\s+/);
|
||||
@@ -668,24 +672,49 @@ export class TahesabPersonLinkService {
|
||||
error?: string;
|
||||
}> = [];
|
||||
|
||||
const existingLinks = await this.prisma.personAccountingLink.findMany({
|
||||
where: { connectionId: dto.connectionId, moshtariCode: { in: codes } },
|
||||
select: { moshtariCode: true, personId: true },
|
||||
});
|
||||
const linkedByCode = new Map(existingLinks.map((l) => [l.moshtariCode, l.personId]));
|
||||
const pending = codes.filter((c) => !linkedByCode.has(c));
|
||||
|
||||
let bulkApi: TahesabApi | undefined;
|
||||
const rowsByCode = new Map<number, Record<string, unknown>>();
|
||||
if (pending.length) {
|
||||
bulkApi = (await this.connections.getApi(organizationId, dto.connectionId)).api;
|
||||
const minCode = Math.min(...pending);
|
||||
const maxCode = Math.max(...pending);
|
||||
if (maxCode - minCode <= 2000) {
|
||||
try {
|
||||
const raw = await bulkApi.doListMoshtariByCode(minCode, maxCode);
|
||||
for (const row of asMoshtariList(raw)) {
|
||||
const code = pickCode(row);
|
||||
if (code != null) rowsByCode.set(code, row);
|
||||
}
|
||||
} catch {
|
||||
/* fallback: per-code fetch inside importFromTahesab */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const moshtariCode of codes) {
|
||||
try {
|
||||
const existing = await this.prisma.personAccountingLink.findFirst({
|
||||
where: { connectionId: dto.connectionId, moshtariCode },
|
||||
});
|
||||
if (existing) {
|
||||
const linkedPersonId = linkedByCode.get(moshtariCode);
|
||||
if (linkedPersonId) {
|
||||
items.push({
|
||||
moshtariCode,
|
||||
ok: true,
|
||||
skipped: true,
|
||||
personId: existing.personId,
|
||||
personId: linkedPersonId,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const r = await this.importFromTahesab(organizationId, {
|
||||
connectionId: dto.connectionId,
|
||||
moshtariCode,
|
||||
});
|
||||
const r = await this.importFromTahesab(
|
||||
organizationId,
|
||||
{ connectionId: dto.connectionId, moshtariCode },
|
||||
bulkApi ? { api: bulkApi, row: rowsByCode.get(moshtariCode) } : undefined,
|
||||
);
|
||||
items.push({ moshtariCode, ok: true, personId: r.personId });
|
||||
} catch (e) {
|
||||
const message =
|
||||
|
||||
@@ -10,8 +10,10 @@ import type {
|
||||
UpdateTahesabConnectionRequest,
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
import {
|
||||
UiBadge,
|
||||
UiBusyOverlay,
|
||||
UiButton,
|
||||
UiCard,
|
||||
UiField,
|
||||
@@ -48,6 +50,13 @@ const sync = reactive({
|
||||
toCode: '',
|
||||
});
|
||||
const syncBusy = ref(false);
|
||||
/** تعداد مشتری در هر درخواست ورود دستهای (برای جلوگیری از تایماوت و نمایش درصد) */
|
||||
const IMPORT_CHUNK = 25;
|
||||
const importOverlay = reactive<{ open: boolean; message: string; progress: number | null }>({
|
||||
open: false,
|
||||
message: '',
|
||||
progress: null,
|
||||
});
|
||||
type SyncResult = {
|
||||
moshtariCode: number | null;
|
||||
name: string;
|
||||
@@ -475,18 +484,72 @@ async function importBulk(mode: 'results' | 'range') {
|
||||
body.toCode = toCode;
|
||||
}
|
||||
}
|
||||
syncBusy.value = true;
|
||||
importOverlay.open = true;
|
||||
importOverlay.progress = 0;
|
||||
importOverlay.message = 'در حال آمادهسازی فهرست مشتریان…';
|
||||
const totals = { imported: 0, skipped: 0, failed: 0 };
|
||||
let firstError = '';
|
||||
try {
|
||||
syncBusy.value = true;
|
||||
const { data } = await api.post<{
|
||||
imported: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
}>('/integrations/tahesab/sync/import-bulk', body);
|
||||
ok.value = `وارد شد: ${data.imported} · قبلاً بود: ${data.skipped} · ناموفق: ${data.failed}`;
|
||||
await searchTahesab(mode === 'range' ? 'range' : 'filter');
|
||||
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);
|
||||
importOverlay.message = `در حال وارد کردن مشتریان… ${toFaDigits(Math.min(i + chunk.length, total))} از ${toFaDigits(total)}`;
|
||||
try {
|
||||
const { data } = await api.post<{
|
||||
imported: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
items?: Array<{ moshtariCode: number; ok: boolean; personId?: string; error?: string }>;
|
||||
}>(
|
||||
'/integrations/tahesab/sync/import-bulk',
|
||||
{ connectionId: sync.connectionId, moshtariCodes: chunk },
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
totals.imported += data.imported;
|
||||
totals.skipped += data.skipped;
|
||||
totals.failed += data.failed;
|
||||
for (const item of data.items ?? []) {
|
||||
if (!item.ok) {
|
||||
if (!firstError && item.error) firstError = item.error;
|
||||
continue;
|
||||
}
|
||||
const row = syncResults.value.find((r) => r.moshtariCode === item.moshtariCode);
|
||||
if (row) {
|
||||
row.alreadyLinked = true;
|
||||
if (item.personId) row.personId = item.personId;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
totals.failed += chunk.length;
|
||||
if (!firstError) firstError = apiErr(e, 'خطا در ارسال');
|
||||
}
|
||||
importOverlay.progress = total ? ((i + chunk.length) / total) * 100 : 100;
|
||||
}
|
||||
|
||||
ok.value = `وارد شد: ${toFaDigits(totals.imported)} · قبلاً بود: ${toFaDigits(totals.skipped)} · ناموفق: ${toFaDigits(totals.failed)}`;
|
||||
if (firstError) error.value = `برخی موارد وارد نشدند: ${firstError}`;
|
||||
} catch (e) {
|
||||
error.value = apiErr(e, 'وارد کردن دستهای ناموفق');
|
||||
} finally {
|
||||
importOverlay.open = false;
|
||||
importOverlay.progress = null;
|
||||
syncBusy.value = false;
|
||||
}
|
||||
}
|
||||
@@ -980,6 +1043,12 @@ onMounted(load);
|
||||
</div>
|
||||
</div>
|
||||
</UiCard>
|
||||
|
||||
<UiBusyOverlay
|
||||
:open="importOverlay.open"
|
||||
:message="importOverlay.message"
|
||||
:progress="importOverlay.progress"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user