feat: filter professionals by city and proximity
- findAllActive now accepts city, lat, lng params - City filter: case-insensitive contains on users.city - Proximity sort: Haversine within city before services/score ranking - Controller exposes ?city=&lat=&lng= query params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
00a6f637f6
commit
d7a0769b46
@@ -16,7 +16,10 @@ export class ProfessionalsController {
|
||||
@Get()
|
||||
findAllActive(@Req() req) {
|
||||
const search = req.query.search as string | undefined;
|
||||
return this.pros.findAllActive(search);
|
||||
const city = req.query.city as string | undefined;
|
||||
const lat = req.query.lat ? parseFloat(req.query.lat as string) : undefined;
|
||||
const lng = req.query.lng ? parseFloat(req.query.lng as string) : undefined;
|
||||
return this.pros.findAllActive(search, city, lat, lng);
|
||||
}
|
||||
|
||||
@Get('pending')
|
||||
|
||||
@@ -20,10 +20,14 @@ export class ProfessionalsService {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async findAllActive(search?: string) {
|
||||
async findAllActive(search?: string, city?: string, lat?: number, lng?: number) {
|
||||
const where: any = { is_active: true };
|
||||
|
||||
if (city && city.trim()) {
|
||||
where.users = { city: { contains: city.trim(), mode: 'insensitive' } };
|
||||
}
|
||||
|
||||
if (search && search.trim()) {
|
||||
// Each word must match at least one of: profession, user name, or city
|
||||
const words = search.trim().split(/\s+/).filter(w => w.length > 1);
|
||||
if (words.length > 0) {
|
||||
where.AND = words.map(word => ({
|
||||
@@ -47,11 +51,28 @@ export class ProfessionalsService {
|
||||
},
|
||||
});
|
||||
|
||||
// Sort: most completed services first, then by average score
|
||||
const sorted = professionals
|
||||
const haversineKm = (lat1: number, lng1: number, lat2: number, lng2: number) => {
|
||||
const R = 6371;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLng = (lng2 - lng1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
};
|
||||
|
||||
const sorted = [...professionals]
|
||||
.sort((a, b) => {
|
||||
const diff = (b._count?.services ?? 0) - (a._count?.services ?? 0);
|
||||
if (diff !== 0) return diff;
|
||||
if (lat !== undefined && lng !== undefined) {
|
||||
const aLat = a.latitude ? Number(a.latitude) : null;
|
||||
const aLng = a.longitude ? Number(a.longitude) : null;
|
||||
const bLat = b.latitude ? Number(b.latitude) : null;
|
||||
const bLng = b.longitude ? Number(b.longitude) : null;
|
||||
if (aLat && aLng && bLat && bLng) {
|
||||
const distDiff = haversineKm(lat, lng, aLat, aLng) - haversineKm(lat, lng, bLat, bLng);
|
||||
if (Math.abs(distDiff) > 0.5) return distDiff;
|
||||
}
|
||||
}
|
||||
const countDiff = (b._count?.services ?? 0) - (a._count?.services ?? 0);
|
||||
if (countDiff !== 0) return countDiff;
|
||||
return Number(b.average_score ?? 0) - Number(a.average_score ?? 0);
|
||||
})
|
||||
.slice(0, 7);
|
||||
|
||||
Reference in New Issue
Block a user