feat: Implement a general location search for agents, querying both legacy city/state/country fields and dynamic profile field values.
This commit is contained in:
@@ -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' };
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user