From 0ad73b1636bfab75cc22f5f38f61d54233137885 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 4 Mar 2026 20:49:37 +0530 Subject: [PATCH] feat: Implement a general location search for agents, querying both legacy city/state/country fields and dynamic profile field values. --- src/agents/agents.service.ts | 85 +++++++++++++++++++++++++++++ src/agents/dto/search-agents.dto.ts | 8 +++ 2 files changed, 93 insertions(+) diff --git a/src/agents/agents.service.ts b/src/agents/agents.service.ts index 7c4b53a..3bdc3b9 100644 --- a/src/agents/agents.service.ts +++ b/src/agents/agents.service.ts @@ -181,6 +181,7 @@ export class AgentsService { async searchAgents(dto: SearchAgentsDto) { const { search, + location, city, state, country, @@ -208,6 +209,90 @@ export class AgentsService { ]; } + // General location search - searches across legacy columns AND dynamic field values + if (location) { + // Find agent IDs that have matching city/state in dynamic field values + // City and State are CHECKBOX_GROUP fields stored as JSON arrays in jsonValue + const locationFields = await this.prisma.profileField.findMany({ + where: { slug: { in: ['city', 'state', 'service_areas'] } }, + select: { id: true }, + }); + + const fieldIds = locationFields.map((f) => f.id); + + let matchingAgentIds: string[] = []; + if (fieldIds.length > 0) { + // Search jsonValue (JSON arrays) and textValue using raw SQL for case-insensitive JSON content matching + const locationLower = location.toLowerCase(); + // Also match snake_case version (e.g., "New York" -> "new_york") + const locationSnakeCase = locationLower.replace(/\s+/g, '_'); + + const matchingFieldValues = + await this.prisma.agentProfileFieldValue.findMany({ + where: { + fieldId: { in: fieldIds }, + OR: [ + { + textValue: { contains: location, mode: 'insensitive' }, + }, + ], + }, + select: { agentProfileId: true, jsonValue: true }, + }); + + // Also fetch all field values with jsonValue for these fields to search within JSON arrays + const jsonFieldValues = + await this.prisma.agentProfileFieldValue.findMany({ + where: { + fieldId: { in: fieldIds }, + jsonValue: { not: Prisma.JsonNull }, + }, + select: { agentProfileId: true, jsonValue: true }, + }); + + // Filter JSON array values that contain the location string + const jsonMatchIds = jsonFieldValues + .filter((fv) => { + const val = fv.jsonValue; + if (Array.isArray(val)) { + return val.some((v: unknown) => { + if (typeof v !== 'string') return false; + const vLower = v.toLowerCase(); + return ( + vLower.includes(locationLower) || + vLower.includes(locationSnakeCase) || + locationLower.includes(vLower) || + locationSnakeCase.includes(vLower) + ); + }); + } + if (typeof val === 'string') { + return val.toLowerCase().includes(locationLower); + } + return false; + }) + .map((fv) => fv.agentProfileId); + + const textMatchIds = matchingFieldValues.map( + (fv) => fv.agentProfileId, + ); + matchingAgentIds = [ + ...new Set([...textMatchIds, ...jsonMatchIds]), + ]; + } + + // Search legacy columns OR dynamic field values + where.OR = [ + ...(where.OR || []), + { city: { contains: location, mode: 'insensitive' } }, + { state: { contains: location, mode: 'insensitive' } }, + { country: { contains: location, mode: 'insensitive' } }, + ...(matchingAgentIds.length > 0 + ? [{ id: { in: matchingAgentIds } }] + : []), + ]; + } + if (city) { where.city = { contains: city, mode: 'insensitive' }; } diff --git a/src/agents/dto/search-agents.dto.ts b/src/agents/dto/search-agents.dto.ts index 2910a93..562f87d 100644 --- a/src/agents/dto/search-agents.dto.ts +++ b/src/agents/dto/search-agents.dto.ts @@ -16,6 +16,14 @@ export class SearchAgentsDto { @IsString() search?: string; + @ApiPropertyOptional({ + example: 'New York', + description: 'General location search across city, state, country, and dynamic field values', + }) + @IsOptional() + @IsString() + location?: string; + @ApiPropertyOptional({ example: 'Los Angeles' }) @IsOptional() @IsString()