Fixing the error when importing a large Excel file
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import type { ActivityType, ImportContactsResult } from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import type { Actor } from '../common/record-scope';
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
writeXlsx,
|
||||
type ExcelPreviewResult,
|
||||
} from '../common/excel';
|
||||
import {
|
||||
chunkImportMeta,
|
||||
resolveExcelImportChunk,
|
||||
} from '../common/excel-import-session';
|
||||
|
||||
const ACTIVITY_COLUMNS = [
|
||||
{ header: '_type', key: '_type', width: 12 },
|
||||
@@ -156,15 +160,52 @@ export class ActivitiesExcelService {
|
||||
|
||||
async import(
|
||||
actor: Actor,
|
||||
file: Express.Multer.File,
|
||||
file: Express.Multer.File | undefined,
|
||||
mappingRaw?: string,
|
||||
chunk?: { sessionId?: string; offset?: string | number; limit?: string | number },
|
||||
): Promise<ImportContactsResult> {
|
||||
const rows = await readMappedRows(file, ACTIVITY_ALIASES, parseExcelMapping(mappingRaw));
|
||||
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
||||
let ctx;
|
||||
try {
|
||||
ctx = await resolveExcelImportChunk({
|
||||
organizationId: actor.organizationId,
|
||||
file,
|
||||
sessionId: chunk?.sessionId,
|
||||
offset: chunk?.offset,
|
||||
limit: chunk?.limit,
|
||||
loadRows: () =>
|
||||
readMappedRows(file!, ACTIVITY_ALIASES, parseExcelMapping(mappingRaw)),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof BadRequestException) throw e;
|
||||
const message =
|
||||
e instanceof Error ? e.message : 'خواندن فایل اکسل ناموفق بود';
|
||||
result.errors.push({ row: 1, message });
|
||||
return result;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]!;
|
||||
const rowNum = i + 2;
|
||||
const MAX_ROWS = 25000;
|
||||
if (ctx.total > MAX_ROWS) {
|
||||
result.errors.push({
|
||||
row: 1,
|
||||
message: `فایل بیش از ${MAX_ROWS} ردیف دارد.`,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const userCache = new Map<string, string | null>();
|
||||
const resolveUser = async (name?: string) => {
|
||||
const key = (name ?? '').trim().toLowerCase();
|
||||
if (!key) return null;
|
||||
if (userCache.has(key)) return userCache.get(key) ?? null;
|
||||
const id = await this.findUserId(actor.organizationId, name);
|
||||
userCache.set(key, id);
|
||||
return id;
|
||||
};
|
||||
|
||||
for (let i = 0; i < ctx.slice.length; i++) {
|
||||
const row = ctx.slice[i]!;
|
||||
const rowNum = ctx.offset + i + 2;
|
||||
if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'activity') {
|
||||
continue;
|
||||
}
|
||||
@@ -191,13 +232,13 @@ export class ActivitiesExcelService {
|
||||
.filter(Boolean);
|
||||
const assigneeIds: string[] = [];
|
||||
for (const name of assigneeNames) {
|
||||
const uid = await this.findUserId(actor.organizationId, name);
|
||||
const uid = await resolveUser(name);
|
||||
if (uid && !assigneeIds.includes(uid)) assigneeIds.push(uid);
|
||||
}
|
||||
if (!assigneeIds.length) assigneeIds.push(actor.userId);
|
||||
const assigneeId = assigneeIds[0]!;
|
||||
const creatorId =
|
||||
(await this.findUserId(actor.organizationId, row.creator)) ?? actor.userId;
|
||||
(await resolveUser(row.creator)) ?? actor.userId;
|
||||
|
||||
const duplicate = dueAt
|
||||
? await this.prisma.activity.findFirst({
|
||||
@@ -215,7 +256,7 @@ export class ActivitiesExcelService {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.prisma.activity.create({
|
||||
const created = await this.prisma.activity.create({
|
||||
data: {
|
||||
organizationId: actor.organizationId,
|
||||
title,
|
||||
@@ -230,10 +271,23 @@ export class ActivitiesExcelService {
|
||||
companyId,
|
||||
dealId,
|
||||
assigneeId,
|
||||
assignees: { connect: assigneeIds.map((id) => ({ id })) },
|
||||
createdById: creatorId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
// M2M مسئولان — اگر مایگریشن روی سرور نباشد، ایمپورت نباید بترکد
|
||||
if (assigneeIds.length) {
|
||||
try {
|
||||
await this.prisma.activity.update({
|
||||
where: { id: created.id },
|
||||
data: {
|
||||
assignees: { set: assigneeIds.map((id) => ({ id })) },
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
/* assigneeId بهتنهایی کافی است */
|
||||
}
|
||||
}
|
||||
result.created += 1;
|
||||
} catch (e) {
|
||||
result.errors.push({
|
||||
@@ -243,7 +297,7 @@ export class ActivitiesExcelService {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return { ...result, ...chunkImportMeta(ctx) };
|
||||
}
|
||||
|
||||
private async findPersonId(organizationId: string, row: Record<string, string>) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
@@ -28,6 +29,11 @@ import {
|
||||
} from './dto';
|
||||
import { brand } from '@mento/shared';
|
||||
|
||||
const excelUpload = FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 15_000_000 },
|
||||
});
|
||||
|
||||
@Controller('activities')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ActivitiesController {
|
||||
@@ -86,29 +92,32 @@ export class ActivitiesController {
|
||||
}
|
||||
|
||||
@Post('import/preview')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 15_000_000 },
|
||||
}),
|
||||
)
|
||||
importPreview(@UploadedFile() file: Express.Multer.File) {
|
||||
@UseInterceptors(excelUpload)
|
||||
importPreview(@UploadedFile() file?: Express.Multer.File) {
|
||||
if (!file?.buffer?.length) {
|
||||
throw new BadRequestException('فایل ارسال نشده');
|
||||
}
|
||||
return this.excelService.preview(file);
|
||||
}
|
||||
|
||||
@Post('import')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 15_000_000 },
|
||||
}),
|
||||
)
|
||||
@UseInterceptors(excelUpload)
|
||||
import(
|
||||
@CurrentUser() user: Actor,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
@Body('mapping') mapping?: string,
|
||||
@Body('sessionId') sessionId?: string,
|
||||
@Body('offset') offset?: string,
|
||||
@Body('limit') limit?: string,
|
||||
) {
|
||||
return this.excelService.import(user, file, mapping);
|
||||
if (!file?.buffer?.length && !sessionId) {
|
||||
throw new BadRequestException('فایل ارسال نشده');
|
||||
}
|
||||
return this.excelService.import(user, file, mapping, {
|
||||
sessionId,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
Reference in New Issue
Block a user