Filtering tasks and assigning a person in charge during Excel import

This commit is contained in:
2026-10-03 12:00:46 +03:30
parent 5784b09232
commit 989c03b381
7 changed files with 315 additions and 18 deletions
@@ -3,7 +3,7 @@ import type { ImportContactsResult, PersonMood } from '@mento/shared';
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import type { Actor } from '../common/record-scope';
import { ownerWhere } from '../common/record-scope';
import { ownerWhere, resolveOwnerId } from '../common/record-scope';
import {
formatJalaliDate,
parseExcelMapping,
@@ -99,6 +99,12 @@ const PERSON_ALIASES: Record<string, string> = {
تلفن2: 'phone2',
تلفنهایدیگرمشتری: 'otherPhones',
مسئولمشتری: 'ownerName',
مسئول: 'ownerName',
مسئولفروش: 'ownerName',
کارشناسفروش: 'ownerName',
کارشناس: 'ownerName',
owner: 'ownerName',
ownername: 'ownerName',
تلفنثابتشرکت: 'companyPhone',
کددیدارشرکت: 'companyDidarId',
مجوزمشاهدهمشتری: 'visibility',
@@ -114,11 +120,14 @@ const PERSON_FIELDS = [
{ key: 'firstName', label: 'نام' },
{ key: 'lastName', label: 'نام خانوادگی' },
{ key: 'title', label: 'نام کامل / عنوان' },
{ key: 'didarId', label: 'کد مبدأ (کد دیدار)' },
{ key: 'customerCode', label: 'کد مشتری' },
{ key: 'mobile', label: 'موبایل' },
{ key: 'phone', label: 'تلفن' },
{ key: 'email', label: 'ایمیل' },
{ key: 'position', label: 'سمت' },
{ key: 'companyName', label: 'شرکت' },
{ key: 'ownerName', label: 'کارشناس فروش' },
{ key: 'notes', label: 'یادداشت' },
{ key: 'mood', label: 'وضعیت مشتری' },
];
@@ -335,6 +344,7 @@ export class ContactsExcelService {
): Promise<ImportContactsResult> {
const rows = await readMappedRows(file, PERSON_ALIASES, parseExcelMapping(mappingRaw));
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
const ownerCache = new Map<string, string | null>();
for (let i = 0; i < rows.length; i++) {
const row = rows[i]!;
@@ -363,6 +373,19 @@ export class ContactsExcelService {
companyId = await this.findOrCreateCompanyId(actor, companyName);
}
const ownerName = row.ownerName?.trim() ?? '';
let ownerId: string | undefined;
let ownerMissing = false;
if (ownerName) {
const key = ownerName.toLowerCase();
if (!ownerCache.has(key)) {
ownerCache.set(key, await this.findUserId(actor.organizationId, ownerName));
}
const matched = ownerCache.get(key);
if (matched) ownerId = matched;
else ownerMissing = true;
}
const customFields: Record<string, string> = {};
for (const key of CUSTOM_KEYS) {
const value = row[key]?.trim();
@@ -380,9 +403,16 @@ export class ContactsExcelService {
notes: row.notes || undefined,
mood: parseMood(row.mood) ?? parseMood(row.group),
companyId,
ownerId: resolveOwnerId(actor, ownerId ?? null),
customFields,
});
result.created += 1;
if (ownerMissing) {
result.errors.push({
row: rowNum,
message: `کارشناس فروش «${ownerName}» در تیم پیدا نشد؛ به کاربر ایمپورتر اختصاص داده شد`,
});
}
} catch (e) {
if (e instanceof ConflictException) {
result.skipped += 1;
@@ -453,4 +483,25 @@ export class ContactsExcelService {
const created = await this.companies.create(actor, { name });
return created.id;
}
private async findUserId(organizationId: string, name?: string) {
const n = name?.trim();
if (!n) return null;
const exact = await this.prisma.user.findFirst({
where: {
fullName: { equals: n, mode: 'insensitive' },
memberships: { some: { organizationId } },
},
select: { id: true },
});
if (exact) return exact.id;
const partial = await this.prisma.user.findFirst({
where: {
fullName: { contains: n, mode: 'insensitive' },
memberships: { some: { organizationId } },
},
select: { id: true },
});
return partial?.id ?? null;
}
}