feat: rank professionals by completed services + score, search by name/profession/city

- GET /professionals now accepts ?search= instead of ?city=/page/limit
- Filters each word across profession, user name, and user city (AND of OR)
- Counts completed services per professional via Prisma _count filter
- Sorts: most completed services first, then highest average_score
- Always returns top 7 results

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-12 20:18:14 -05:00
co-authored by Claude Sonnet 4.6
parent 74be21eb4e
commit 00a6f637f6
2 changed files with 38 additions and 22 deletions
@@ -15,10 +15,8 @@ export class ProfessionalsController {
@Get()
findAllActive(@Req() req) {
const page = +(req.query.page || 1);
const limit = +(req.query.limit || 20);
const city = req.query.city as string | undefined;
return this.pros.findAllActive(page, limit, city);
const search = req.query.search as string | undefined;
return this.pros.findAllActive(search);
}
@Get('pending')
@@ -20,25 +20,43 @@ export class ProfessionalsService {
} catch (_) {}
}
async findAllActive(page = 1, limit = 20, city?: string) {
const skip = (page - 1) * limit;
async findAllActive(search?: string) {
const where: any = { is_active: true };
if (city) where.users = { city: { contains: city, mode: 'insensitive' } };
const [data, total] = await Promise.all([
this.prisma.professionals.findMany({
where,
skip,
take: limit,
include: {
users: { select: { id: true, name: true, email: true, phone: true, picture: true, city: true } },
schedules: true,
specializations: true,
payment_methods: true,
},
}),
this.prisma.professionals.count({ where }),
]);
return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit) || 1 } };
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 => ({
OR: [
{ profession: { contains: word, mode: 'insensitive' } },
{ users: { name: { contains: word, mode: 'insensitive' } } },
{ users: { city: { contains: word, mode: 'insensitive' } } },
],
}));
}
}
const professionals = await this.prisma.professionals.findMany({
where,
include: {
users: { select: { id: true, name: true, email: true, phone: true, picture: true, city: true } },
schedules: true,
specializations: true,
payment_methods: true,
_count: { select: { services: { where: { status: 'completed' as any } } } },
},
});
// Sort: most completed services first, then by average score
const sorted = professionals
.sort((a, b) => {
const diff = (b._count?.services ?? 0) - (a._count?.services ?? 0);
if (diff !== 0) return diff;
return Number(b.average_score ?? 0) - Number(a.average_score ?? 0);
})
.slice(0, 7);
return { data: sorted, meta: { total: sorted.length, page: 1, limit: 7, totalPages: 1 } };
}
async findById(id: string) {