Selection of multiple officials and role sub-categories

This commit is contained in:
2026-10-03 13:16:13 +03:30
parent 9f97371391
commit 3b128bd980
14 changed files with 904 additions and 95 deletions
@@ -109,6 +109,7 @@ export class ActivitiesExcelService {
company: { select: { id: true, name: true } },
deal: { select: { id: true, title: true } },
assignee: { select: { fullName: true } },
assignees: { select: { fullName: true } },
createdBy: { select: { fullName: true } },
},
orderBy: [{ dueAt: 'desc' }, { createdAt: 'desc' }],
@@ -124,7 +125,9 @@ export class ActivitiesExcelService {
title: row.title,
body: row.description ?? '',
result: '',
assignee: row.assignee?.fullName ?? '',
assignee: row.assignees?.length
? row.assignees.map((a) => a.fullName).join('، ')
: (row.assignee?.fullName ?? ''),
creator: row.createdBy?.fullName ?? '',
dueDate: formatJalaliDate(row.dueAt),
dueTime: formatClock(row.dueAt),
@@ -182,8 +185,17 @@ export class ActivitiesExcelService {
const personId = await this.findPersonId(actor.organizationId, row);
const companyId = await this.findCompanyId(actor.organizationId, row.companyName);
const dealId = await this.findDealId(actor.organizationId, row.dealTitle);
const assigneeId =
(await this.findUserId(actor.organizationId, row.assignee)) ?? actor.userId;
const assigneeNames = String(row.assignee ?? '')
.split(/[,،]/)
.map((s) => s.trim())
.filter(Boolean);
const assigneeIds: string[] = [];
for (const name of assigneeNames) {
const uid = await this.findUserId(actor.organizationId, 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;
@@ -218,6 +230,7 @@ export class ActivitiesExcelService {
companyId,
dealId,
assigneeId,
assignees: { connect: assigneeIds.map((id) => ({ id })) },
createdById: creatorId,
},
});
+105 -33
View File
@@ -41,6 +41,7 @@ type ActivityRow = {
company: { id: string; name: string } | null;
deal: { id: string; title: string } | null;
assignee: { id: string; fullName: string } | null;
assignees: Array<{ id: string; fullName: string }>;
createdBy: { id: string; fullName: string } | null;
};
@@ -49,6 +50,7 @@ const include = {
company: { select: { id: true, name: true } },
deal: { select: { id: true, title: true } },
assignee: { select: { id: true, fullName: true } },
assignees: { select: { id: true, fullName: true }, orderBy: { fullName: 'asc' as const } },
createdBy: { select: { id: true, fullName: true } },
} satisfies Prisma.ActivityInclude;
@@ -186,8 +188,15 @@ export class ActivitiesService {
): Promise<ActivityDto> {
const linkActor = actor ?? { userId, role: 'owner' };
await this.assertLinks(organizationId, linkActor, dto);
const assigneeId = emptyToNull(dto.assigneeId ?? null) ?? userId;
if (assigneeId) await this.assertUserInOrg(organizationId, assigneeId);
const assigneeIds = await this.normalizeAssigneeIds(
organizationId,
dto.assigneeIds?.length
? dto.assigneeIds
: dto.assigneeId
? [dto.assigneeId]
: [userId],
);
const assigneeId = assigneeIds[0] ?? userId;
const row = await this.prisma.activity.create({
data: {
@@ -206,15 +215,18 @@ export class ActivitiesService {
companyId: emptyToNull(dto.companyId ?? null),
dealId: emptyToNull(dto.dealId ?? null),
assigneeId,
assignees: assigneeIds.length
? { connect: assigneeIds.map((id) => ({ id })) }
: undefined,
createdById: userId,
},
include,
});
if (assigneeId) {
for (const uid of assigneeIds) {
await this.notifications.create({
organizationId,
userId: assigneeId,
userId: uid,
type: 'activity_assigned',
title: 'فعالیت جدید برای شما',
body: row.title,
@@ -262,10 +274,23 @@ export class ActivitiesService {
if ('personId' in dto) data.personId = emptyToNull(dto.personId ?? null);
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
if ('dealId' in dto) data.dealId = emptyToNull(dto.dealId ?? null);
if ('assigneeId' in dto) {
let assigneeConnect: Array<{ id: string }> | undefined;
if (dto.assigneeIds !== undefined) {
const assigneeIds = await this.normalizeAssigneeIds(
organizationId,
dto.assigneeIds,
);
if (!assigneeIds.length) {
throw new BadRequestException('حداقل یک مسئول لازم است');
}
data.assigneeId = assigneeIds[0]!;
assigneeConnect = assigneeIds.map((uid) => ({ id: uid }));
} else if ('assigneeId' in dto) {
const assigneeId = emptyToNull(dto.assigneeId ?? null);
if (assigneeId) await this.assertUserInOrg(organizationId, assigneeId);
data.assigneeId = assigneeId;
if (assigneeId) assigneeConnect = [{ id: assigneeId }];
else assigneeConnect = [];
}
if (dto.status !== undefined) {
data.status = dto.status;
@@ -279,7 +304,12 @@ export class ActivitiesService {
});
const row = await this.prisma.activity.update({
where: { id },
data,
data: {
...data,
...(assigneeConnect
? { assignees: { set: assigneeConnect } }
: {}),
},
include,
});
if (dto.status === 'done' && existing?.status !== 'done') {
@@ -340,37 +370,55 @@ export class ActivitiesService {
},
): 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 } : {}),
...(q?.trim()
const scope: Prisma.ActivityWhereInput =
desk === 'mine'
? {
OR: [
{ title: { contains: q.trim(), mode: 'insensitive' } },
{ description: { contains: q.trim(), mode: 'insensitive' } },
{
person: {
OR: [
{ firstName: { contains: q.trim(), mode: 'insensitive' } },
{ lastName: { contains: q.trim(), mode: 'insensitive' } },
],
},
},
{ deal: { title: { contains: q.trim(), mode: 'insensitive' } } },
{ assigneeId: actor.userId },
{ assignees: { some: { id: actor.userId } } },
],
}
: {}),
: assigneeWhere(actor);
const facetAssignee = facets?.assigneeId?.trim();
const andParts: Prisma.ActivityWhereInput[] = [
scope,
...(facets?.type ? [{ type: facets.type }] : []),
...(facets?.status ? [{ status: facets.status }] : []),
...(facetAssignee
? [
{
OR: [
{ assigneeId: facetAssignee },
{ assignees: { some: { id: facetAssignee } } },
],
},
]
: []),
...(entity?.personId ? [{ personId: entity.personId }] : []),
...(entity?.companyId ? [{ companyId: entity.companyId }] : []),
...(entity?.dealId ? [{ dealId: entity.dealId }] : []),
];
if (q?.trim()) {
const needle = q.trim();
andParts.push({
OR: [
{ title: { contains: needle, mode: 'insensitive' } },
{ description: { contains: needle, mode: 'insensitive' } },
{
person: {
OR: [
{ firstName: { contains: needle, mode: 'insensitive' } },
{ lastName: { contains: needle, mode: 'insensitive' } },
],
},
},
{ deal: { title: { contains: needle, mode: 'insensitive' } } },
],
});
}
const base: Prisma.ActivityWhereInput = {
organizationId,
AND: andParts,
};
const fromDate = range?.from ? new Date(range.from) : null;
@@ -517,11 +565,35 @@ export class ActivitiesService {
company: row.company,
deal: row.deal,
assignee: row.assignee,
assignees: (() => {
const list = [...(row.assignees ?? [])];
if (row.assignee && !list.some((a) => a.id === row.assignee!.id)) {
list.unshift(row.assignee);
}
return list;
})(),
createdBy: row.createdBy,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
private async normalizeAssigneeIds(
organizationId: string,
raw?: string[] | null,
): Promise<string[]> {
const ids = [
...new Set((raw ?? []).map((id) => id.trim()).filter(Boolean)),
];
if (!ids.length) return [];
const count = await this.prisma.user.count({
where: { organizationId, id: { in: ids }, isActive: true },
});
if (count !== ids.length) {
throw new BadRequestException('یکی از مسئولان انتخاب‌شده معتبر نیست');
}
return ids;
}
}
function emptyToNull(value?: string | null): string | null {
+10
View File
@@ -148,6 +148,11 @@ export class CreateActivityDto {
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
assigneeId?: string | null;
@IsOptional()
@IsArray()
@IsString({ each: true })
assigneeIds?: string[];
}
export class UpdateActivityDto {
@@ -223,4 +228,9 @@ export class UpdateActivityDto {
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
assigneeId?: string | null;
@IsOptional()
@IsArray()
@IsString({ each: true })
assigneeIds?: string[];
}