feat(activities): filter the desk from each column

Assignees are searched instead of listed in full, and start time only filters whether a time is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 21:20:42 +03:30
co-authored by Cursor
parent d5dd13863a
commit 4af248b06a
6 changed files with 769 additions and 83 deletions
@@ -193,6 +193,9 @@ export class ActivitiesController {
typeDefId: query.typeDefId,
status: query.status,
assigneeId: query.assigneeId,
title: query.title,
customer: query.customer,
due: query.due,
boardId: query.boardId,
},
);
@@ -226,6 +229,9 @@ export class ActivitiesController {
typeDefId: query.typeDefId,
status: query.status,
assigneeId: query.assigneeId,
title: query.title,
customer: query.customer,
due: query.due,
boardId: query.boardId,
},
query.stageId ? { stageId: query.stageId } : undefined,
+66 -21
View File
@@ -53,6 +53,10 @@ type ListFacets = {
typeDefId?: string;
status?: ActivityDto['status'];
assigneeId?: string;
title?: string;
customer?: string;
/** set = زمان شروع دارد، unset = ندارد */
due?: 'set' | 'unset';
/** گروه فعالیت؛ خالی = همهٔ گروه‌ها */
boardId?: string;
};
@@ -864,11 +868,15 @@ export class ActivitiesService {
: activityAccessWhere(actor);
const facetAssignee = facets?.assigneeId?.trim();
const facetTypeDef = facets?.typeDefId?.trim();
const facetTitle = facets?.title?.trim();
const facetCustomer = facets?.customer?.trim();
const andParts: Prisma.ActivityWhereInput[] = [
scope,
...(facets?.type ? [{ type: facets.type }] : []),
...(facetTypeDef ? [this.types.whereForType(typeRows, facetTypeDef)] : []),
...(facets?.status ? [{ status: facets.status }] : []),
...(facetTitle ? [{ title: ci(facetTitle) }] : []),
...(facetCustomer ? [this.customerWhere(facetCustomer)] : []),
...(facetAssignee
? [
{
@@ -915,15 +923,18 @@ export class ActivitiesService {
const fromDate = range?.from ? new Date(range.from) : null;
const toDate = range?.to ? new Date(range.to) : null;
if (fromDate || toDate) {
return {
...base,
dueAt: {
...(fromDate && !Number.isNaN(fromDate.getTime())
? { gte: fromDate }
: {}),
...(toDate && !Number.isNaN(toDate.getTime()) ? { lt: toDate } : {}),
return this.withDue(
{
...base,
dueAt: {
...(fromDate && !Number.isNaN(fromDate.getTime())
? { gte: fromDate }
: {}),
...(toDate && !Number.isNaN(toDate.getTime()) ? { lt: toDate } : {}),
},
},
};
facets?.due,
);
}
// Explicit status facet overrides the status implied by time tabs.
@@ -937,28 +948,62 @@ export class ActivitiesService {
switch (filter) {
case 'today':
return { ...base, ...impliedOpen, dueAt: { gte: start, lt: end } };
return this.withDue(
{ ...base, ...impliedOpen, dueAt: { gte: start, lt: end } },
facets?.due,
);
case 'overdue':
// عقب‌افتاده ذاتاً یعنی باز و موعدگذشته
return {
...base,
...(facets?.status ? { status: facets.status } : { status: 'open' as const }),
dueAt: { lt: start },
};
return this.withDue(
{
...base,
...(facets?.status ? { status: facets.status } : { status: 'open' as const }),
dueAt: { lt: start },
},
facets?.due,
);
case 'upcoming':
return {
...base,
...impliedOpen,
OR: [{ dueAt: { gte: end } }, { dueAt: null }],
};
return this.withDue(
{
...base,
...impliedOpen,
OR: [{ dueAt: { gte: end } }, { dueAt: null }],
},
facets?.due,
);
case 'done':
return { ...base, ...impliedDone };
return this.withDue({ ...base, ...impliedDone }, facets?.due);
case 'all':
default:
return base;
return this.withDue(base, facets?.due);
}
}
/** نام شخص (چندکلمه‌ای) یا نام شرکت */
private customerWhere(term: string): Prisma.ActivityWhereInput {
const parts = term.split(/\s+/).filter(Boolean);
const person: Prisma.PersonWhereInput =
parts.length <= 1
? { OR: [{ firstName: ci(term) }, { lastName: ci(term) }] }
: {
AND: parts.map((part) => ({
OR: [{ firstName: ci(part) }, { lastName: ci(part) }],
})),
};
return {
OR: [{ person }, { company: { name: ci(term) } }],
};
}
/** وجود یا نبود زمان شروع، روی هر فیلتر زمانی */
private withDue(
where: Prisma.ActivityWhereInput,
due?: 'set' | 'unset',
): Prisma.ActivityWhereInput {
if (due !== 'set' && due !== 'unset') return where;
return { AND: [where, due === 'set' ? { dueAt: { not: null } } : { dueAt: null }] };
}
private async fireDone(
organizationId: string,
row: {
+17
View File
@@ -69,6 +69,23 @@ export class ListActivitiesQueryDto {
@IsString()
assigneeId?: string;
/** فیلتر عنوان فعالیت، جدا از جستجوی کلی */
@IsOptional()
@IsString()
@MaxLength(120)
title?: string;
/** نام شخص یا شرکتِ مشتری */
@IsOptional()
@IsString()
@MaxLength(120)
customer?: string;
/** زمان شروع: set = دارد، unset = ندارد */
@IsOptional()
@IsIn(['set', 'unset'])
due?: 'set' | 'unset';
/** نوع فعالیت (تعریف‌شده در سازمان) */
@IsOptional()
@IsString()