feat: multi-select contacts filter panel for groups sources and mood

This commit is contained in:
2026-10-21 18:07:21 +03:30
parent 29d99cbded
commit 50d7cad66e
6 changed files with 573 additions and 144 deletions
+13
View File
@@ -79,6 +79,19 @@ export function buildSearchWhere<W extends object>(
return (parts.length === 1 ? parts[0] : { AND: parts }) as unknown as W;
}
/** پارامتر چندتایی کوئری: «a,b,c» → ['a','b','c'] */
export function splitCsv(raw: string | null | undefined, max = 50): string[] {
if (!raw) return [];
return [
...new Set(
String(raw)
.split(',')
.map((v) => v.trim())
.filter(Boolean),
),
].slice(0, max);
}
/** کمک برای فیلدهای متنی ساده: `{ field: { contains, mode: 'insensitive' } }` */
export function ci(term: string) {
return { contains: term, mode: 'insensitive' as const };
+3 -3
View File
@@ -25,7 +25,7 @@ import {
} from '../common/record-scope';
import { normalizeTags } from '../common/tags';
import { normalizePersonImportSource } from './person-source';
import { buildSearchWhere, ci } from '../common/search';
import { buildSearchWhere, ci, splitCsv } from '../common/search';
import { GamificationService } from '../gamification/gamification.service';
import {
CustomerGroupsService,
@@ -105,10 +105,10 @@ export class CompaniesService {
{ city: ci(t) },
{ province: ci(t) },
]);
const groupId = opts?.groupId?.trim();
const groupIds = splitCsv(opts?.groupId);
const and: Prisma.CompanyWhereInput[] = [
...(search ? [search] : []),
...(groupId ? [{ groupLinks: { some: { groupId } } }] : []),
...(groupIds.length ? [{ groupLinks: { some: { groupId: { in: groupIds } } } }] : []),
];
const where: Prisma.CompanyWhereInput = {
organizationId: actor.organizationId,
+8 -6
View File
@@ -50,18 +50,20 @@ export class ListQueryDto {
@Max(100)
limit?: number = 20;
/** وضعیت مشتری؛ چندتایی با کاما */
@IsOptional()
@IsIn([...PERSON_MOODS])
mood?: (typeof PERSON_MOODS)[number];
@IsString()
@MaxLength(120)
mood?: string;
@IsOptional()
@IsIn(['name', 'newest'])
sort?: 'name' | 'newest';
/** فیلتر منبع ورود شخص */
/** فیلتر منبع ورود شخص؛ چندتایی با کاما */
@IsOptional()
@IsString()
@MaxLength(40)
@MaxLength(400)
importSource?: string;
/** استان شرکت مرتبط */
@@ -87,10 +89,10 @@ export class ListQueryDto {
@MaxLength(12)
disc?: string;
/** فیلتر گروه مشتری */
/** فیلتر گروه مشتری؛ چندتایی با کاما (هر کدام از گروه‌ها) */
@IsOptional()
@IsString()
@MaxLength(40)
@MaxLength(2000)
groupId?: string;
}
+13 -12
View File
@@ -17,7 +17,7 @@ import type {
PersonMood,
UpdatePersonRequest,
} from '@mento/shared';
import { FILTERED_IDS_MAX } from '@mento/shared';
import { FILTERED_IDS_MAX, PERSON_MOODS } from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { OutboundSmsLogService } from '../outbound-sms/outbound-sms-log.service';
@@ -45,7 +45,7 @@ import { SmsGatewayService } from '../sms-billing/sms-gateway.service';
import { selectMonthBirthdays } from './birthday-month';
import { renderSmsText } from '../common/sms-text';
import { normalizeTags } from '../common/tags';
import { buildSearchWhere, ci } from '../common/search';
import { buildSearchWhere, ci, splitCsv } from '../common/search';
import {
normalizePersonImportSource,
personImportSourceLabel,
@@ -150,7 +150,7 @@ export class PeopleService implements OnModuleInit {
actor: Actor,
q?: string,
opts?: {
mood?: PersonMood;
mood?: string;
importSource?: string;
province?: string;
city?: string;
@@ -159,9 +159,11 @@ export class PeopleService implements OnModuleInit {
groupId?: string;
},
): Prisma.PersonWhereInput {
const sourceFilter = opts?.importSource?.trim()
? normalizePersonImportSource(opts.importSource)
: undefined;
const sources = [...new Set(splitCsv(opts?.importSource).map(normalizePersonImportSource))];
const moods = splitCsv(opts?.mood).filter((m): m is PersonMood =>
(PERSON_MOODS as readonly string[]).includes(m),
);
const groupIds = splitCsv(opts?.groupId);
const search = buildSearchWhere<Prisma.PersonWhereInput>(q, (t) => [
{ firstName: ci(t) },
{ lastName: ci(t) },
@@ -201,13 +203,12 @@ export class PeopleService implements OnModuleInit {
}
const disc = discFilter(opts?.disc);
if (disc) and.push(disc);
const groupId = opts?.groupId?.trim();
if (groupId) and.push({ groupLinks: { some: { groupId } } });
if (groupIds.length) and.push({ groupLinks: { some: { groupId: { in: groupIds } } } });
return {
organizationId: actor.organizationId,
...ownerWhere(actor),
...(opts?.mood ? { mood: opts.mood } : {}),
...(sourceFilter ? { importSource: sourceFilter } : {}),
...(moods.length ? { mood: { in: moods } } : {}),
...(sources.length ? { importSource: { in: sources } } : {}),
...(and.length ? { AND: and } : {}),
};
}
@@ -218,7 +219,7 @@ export class PeopleService implements OnModuleInit {
page = 1,
limit = 20,
opts?: {
mood?: PersonMood;
mood?: string;
sort?: 'name' | 'newest';
importSource?: string;
province?: string;
@@ -257,7 +258,7 @@ export class PeopleService implements OnModuleInit {
actor: Actor,
q?: string,
opts?: {
mood?: PersonMood;
mood?: string;
importSource?: string;
province?: string;
city?: string;