Filtering tasks and assigning a person in charge during Excel import
This commit is contained in:
@@ -63,6 +63,11 @@ export class ActivitiesController {
|
||||
to: query.to,
|
||||
},
|
||||
query.desk,
|
||||
{
|
||||
type: query.type,
|
||||
status: query.status,
|
||||
assigneeId: query.assigneeId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,11 @@ export class ActivitiesService {
|
||||
to?: string;
|
||||
},
|
||||
desk?: 'mine' | 'team',
|
||||
facets?: {
|
||||
type?: ActivityDto['type'];
|
||||
status?: ActivityDto['status'];
|
||||
assigneeId?: string;
|
||||
},
|
||||
): Promise<PaginatedResponse<ActivityDto>> {
|
||||
const where = this.buildWhere(
|
||||
organizationId,
|
||||
@@ -138,6 +143,7 @@ export class ActivitiesService {
|
||||
entity,
|
||||
range,
|
||||
desk,
|
||||
facets,
|
||||
);
|
||||
const take = Math.min(Math.max(limit, 1), 200);
|
||||
const [rows, total] = await this.prisma.$transaction([
|
||||
@@ -327,13 +333,24 @@ export class ActivitiesService {
|
||||
to?: string;
|
||||
},
|
||||
desk?: 'mine' | 'team',
|
||||
facets?: {
|
||||
type?: ActivityDto['type'];
|
||||
status?: ActivityDto['status'];
|
||||
assigneeId?: string;
|
||||
},
|
||||
): Prisma.ActivityWhereInput {
|
||||
const { start, end } = dayBounds();
|
||||
const scope =
|
||||
desk === 'mine' ? { assigneeId: actor.userId } : assigneeWhere(actor);
|
||||
const facetWhere: Prisma.ActivityWhereInput = {
|
||||
...(facets?.type ? { type: facets.type } : {}),
|
||||
...(facets?.status ? { status: facets.status } : {}),
|
||||
...(facets?.assigneeId?.trim() ? { assigneeId: facets.assigneeId.trim() } : {}),
|
||||
};
|
||||
const base: Prisma.ActivityWhereInput = {
|
||||
organizationId,
|
||||
...scope,
|
||||
...facetWhere,
|
||||
...(entity?.personId ? { personId: entity.personId } : {}),
|
||||
...(entity?.companyId ? { companyId: entity.companyId } : {}),
|
||||
...(entity?.dealId ? { dealId: entity.dealId } : {}),
|
||||
@@ -370,19 +387,23 @@ export class ActivitiesService {
|
||||
};
|
||||
}
|
||||
|
||||
// Explicit status facet overrides the status implied by time tabs.
|
||||
const impliedOpen = facets?.status ? { status: facets.status } : { status: 'open' as const };
|
||||
const impliedDone = facets?.status ? { status: facets.status } : { status: 'done' as const };
|
||||
|
||||
switch (filter) {
|
||||
case 'today':
|
||||
return { ...base, status: 'open', dueAt: { gte: start, lt: end } };
|
||||
return { ...base, ...impliedOpen, dueAt: { gte: start, lt: end } };
|
||||
case 'overdue':
|
||||
return { ...base, status: 'open', dueAt: { lt: start } };
|
||||
return { ...base, ...impliedOpen, dueAt: { lt: start } };
|
||||
case 'upcoming':
|
||||
return {
|
||||
...base,
|
||||
status: 'open',
|
||||
...impliedOpen,
|
||||
OR: [{ dueAt: { gte: end } }, { dueAt: null }],
|
||||
};
|
||||
case 'done':
|
||||
return { ...base, status: 'done' };
|
||||
return { ...base, ...impliedDone };
|
||||
case 'all':
|
||||
default:
|
||||
return base;
|
||||
|
||||
@@ -32,6 +32,18 @@ export class ListActivitiesQueryDto {
|
||||
@IsIn(['mine', 'team'])
|
||||
desk?: 'mine' | 'team';
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ACTIVITY_TYPES)
|
||||
type?: (typeof ACTIVITY_TYPES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ACTIVITY_STATUSES)
|
||||
status?: (typeof ACTIVITY_STATUSES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assigneeId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
personId?: string;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user