Edit import excel

This commit is contained in:
2026-09-19 15:30:31 +03:30
parent 9c7dad3677
commit 4776d1a3c3
22 changed files with 731 additions and 261 deletions
@@ -56,13 +56,20 @@ export class CompaniesController {
return new StreamableFile(buffer);
}
@Post('import/preview')
@UseInterceptors(FileInterceptor('file'))
previewImport(@UploadedFile() file: Express.Multer.File) {
return this.excelService.previewCompanies(file);
}
@Post('import')
@UseInterceptors(FileInterceptor('file'))
import(
@CurrentUser() user: Actor,
@UploadedFile() file: Express.Multer.File,
@Body('mapping') mapping?: string,
) {
return this.excelService.importCompanies(user, file);
return this.excelService.importCompanies(user, file, mapping);
}
@Get(':id')
@@ -6,8 +6,11 @@ import type { Actor } from '../common/record-scope';
import { ownerWhere } from '../common/record-scope';
import {
formatJalaliDate,
parseExcelMapping,
previewExcel,
readMappedRows,
writeXlsx,
type ExcelPreviewResult,
} from '../common/excel';
import { CompaniesService } from './companies.service';
import { PeopleService } from './people.service';
@@ -107,6 +110,19 @@ const PERSON_ALIASES: Record<string, string> = {
ستاره: 'star',
};
const PERSON_FIELDS = [
{ key: 'firstName', label: 'نام' },
{ key: 'lastName', label: 'نام خانوادگی' },
{ key: 'title', label: 'نام کامل / عنوان' },
{ key: 'mobile', label: 'موبایل' },
{ key: 'phone', label: 'تلفن' },
{ key: 'email', label: 'ایمیل' },
{ key: 'position', label: 'سمت' },
{ key: 'companyName', label: 'شرکت' },
{ key: 'notes', label: 'یادداشت' },
{ key: 'mood', label: 'وضعیت مشتری' },
];
const COMPANY_ALIASES: Record<string, string> = {
name: 'name',
نام: 'name',
@@ -126,6 +142,16 @@ const COMPANY_ALIASES: Record<string, string> = {
یادداشت: 'notes',
};
const COMPANY_FIELDS = [
{ key: 'name', label: 'نام شرکت', required: true },
{ key: 'nationalId', label: 'شناسه ملی' },
{ key: 'phone', label: 'تلفن' },
{ key: 'email', label: 'ایمیل' },
{ key: 'website', label: 'وب‌سایت' },
{ key: 'address', label: 'آدرس' },
{ key: 'notes', label: 'یادداشت' },
];
const CONTACT_COLUMNS = [
{ header: '_type', key: '_type', width: 12 },
{ header: 'کد دیدار مشتری', key: 'didarId', width: 16 },
@@ -294,11 +320,20 @@ export class ContactsExcelService {
);
}
previewPeople(file: Express.Multer.File): Promise<ExcelPreviewResult> {
return previewExcel(file, PERSON_ALIASES, PERSON_FIELDS);
}
previewCompanies(file: Express.Multer.File): Promise<ExcelPreviewResult> {
return previewExcel(file, COMPANY_ALIASES, COMPANY_FIELDS);
}
async importPeople(
actor: Actor,
file: Express.Multer.File,
mappingRaw?: string,
): Promise<ImportContactsResult> {
const rows = await readMappedRows(file, PERSON_ALIASES);
const rows = await readMappedRows(file, PERSON_ALIASES, parseExcelMapping(mappingRaw));
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
for (let i = 0; i < rows.length; i++) {
@@ -307,9 +342,18 @@ export class ContactsExcelService {
if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'contact') {
continue;
}
const names = splitDidarName(row.firstName ?? '', row.lastName ?? '', row.title);
if (!names) {
result.errors.push({ row: rowNum, message: 'نام مشتری الزامی است' });
const names = splitDidarName(row.firstName ?? '', row.lastName ?? '', row.title) ?? {
firstName: (row.firstName ?? '').trim().slice(0, 80),
lastName: (row.lastName ?? '').trim().slice(0, 80),
};
if (
!names.firstName &&
!names.lastName &&
!row.mobile?.trim() &&
!row.email?.trim() &&
!row.phone?.trim()
) {
result.errors.push({ row: rowNum, message: 'حداقل نام، موبایل، تلفن یا ایمیل لازم است' });
continue;
}
@@ -357,8 +401,9 @@ export class ContactsExcelService {
async importCompanies(
actor: Actor,
file: Express.Multer.File,
mappingRaw?: string,
): Promise<ImportContactsResult> {
const rows = await readMappedRows(file, COMPANY_ALIASES);
const rows = await readMappedRows(file, COMPANY_ALIASES, parseExcelMapping(mappingRaw));
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
for (let i = 0; i < rows.length; i++) {
+4 -6
View File
@@ -135,15 +135,15 @@ export class UpdateCompanyDto {
}
export class CreatePersonDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(80)
firstName!: string;
firstName?: string;
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(80)
lastName!: string;
lastName?: string;
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@@ -192,13 +192,11 @@ export class CreatePersonDto {
export class UpdatePersonDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(80)
firstName?: string;
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(80)
lastName?: string;
+10 -1
View File
@@ -57,13 +57,22 @@ export class PeopleController {
return new StreamableFile(buffer);
}
@Post('import/preview')
@UseInterceptors(FileInterceptor('file'))
previewImport(
@UploadedFile() file: Express.Multer.File,
) {
return this.excelService.previewPeople(file);
}
@Post('import')
@UseInterceptors(FileInterceptor('file'))
import(
@CurrentUser() user: Actor,
@UploadedFile() file: Express.Multer.File,
@Body('mapping') mapping?: string,
) {
return this.excelService.importPeople(user, file);
return this.excelService.importPeople(user, file, mapping);
}
@Get(':id')
+2 -2
View File
@@ -134,8 +134,8 @@ export class PeopleService {
const data: Prisma.PersonUncheckedCreateInput = {
organizationId: actor.organizationId,
firstName: dto.firstName.trim(),
lastName: dto.lastName.trim(),
firstName: (dto.firstName ?? '').trim(),
lastName: (dto.lastName ?? '').trim(),
email: normalizeEmail(dto.email),
phone: normalizePhone(dto.phone),
mobile: normalizePhone(dto.mobile),