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) {
|
async searchAgents(dto: SearchAgentsDto) {
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
|
location,
|
||||||
city,
|
city,
|
||||||
state,
|
state,
|
||||||
country,
|
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) {
|
if (city) {
|
||||||
where.city = { contains: city, mode: 'insensitive' };
|
where.city = { contains: city, mode: 'insensitive' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export class SearchAgentsDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
search?: string;
|
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' })
|
@ApiPropertyOptional({ example: 'Los Angeles' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
Reference in New Issue
Block a user