From 00a6f637f6748b18c23aac13c6c40a6bd270a083 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:18:14 -0500 Subject: [PATCH] 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 --- .../professionals/professionals.controller.ts | 6 +-- .../professionals/professionals.service.ts | 54 ++++++++++++------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/backend/src/professionals/professionals.controller.ts b/backend/src/professionals/professionals.controller.ts index 1c15fe0..8c44a3f 100644 --- a/backend/src/professionals/professionals.controller.ts +++ b/backend/src/professionals/professionals.controller.ts @@ -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') diff --git a/backend/src/professionals/professionals.service.ts b/backend/src/professionals/professionals.service.ts index c6c51db..2c76cb8 100644 --- a/backend/src/professionals/professionals.service.ts +++ b/backend/src/professionals/professionals.service.ts @@ -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) {