From 1e45b12aecb1fc2a6ae383fe13ec8106686555f1 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 18 Jun 2026 19:16:04 +0330 Subject: [PATCH] feat(doctor): add filtering logic for clinic doctor list with pagination and sorting --- docs/api/clinic.md | 52 ++++++++++++++++++---- src/Clinic/Controller/ClinicController.php | 25 +++++++++-- src/Doctor/Repository/DoctorRepository.php | 51 +++++++++++++++++++++ 3 files changed, 115 insertions(+), 13 deletions(-) diff --git a/docs/api/clinic.md b/docs/api/clinic.md index 65a08f23..10296c3f 100644 --- a/docs/api/clinic.md +++ b/docs/api/clinic.md @@ -246,22 +246,56 @@ Get doctors associated with a clinic. |-------|------|-------------| | `clinicUuid` | string (UUID) | Clinic UUID | +### Query Parameters +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `page` | integer | ❌ | Default: 1 | +| `limit` | integer | ❌ | Default: 10, max 50 | +| `name` | string | ❌ | Filter by doctor name (`LIKE`) | +| `specialty` | integer | ❌ | Specialty id | +| `gender` | string | ❌ | `man` / `woman` | +| `degree` | string | ❌ | `expert` / `general` / `specialist` / `subspecialistplus` | +| `active` | 0\|1 | ❌ | Only doctors with appointments enabled | +| `sort` | string | ❌ | `ASC` / `DESC` by rating (default `DESC`) | + +> Filters apply **only within this clinic's** linked doctors. + +> ⚠️ **Double-nested:** the doctors array is at `data.data` (extract with `data?.data?.data`); pagination is at `data.meta`. + ### Response `200` ```json { "success": true, - "data": [ - { - "uuid": "...", - "title": "دکتر علی احمدی", - "degree": "متخصص", - "doctor_rate": 4.5, - "image": "https://..." - } - ] + "data": { + "data": [ + { + "id": "1207", + "uuid": "...", + "name": "دکتر آرمان رضایی", + "gender": "man", + "degree": "specialist", + "img": [], + "specialties": [{ "uuid": "...", "id": "2", "name": "داخلی عمومی" }], + "satisfaction": "96", + "point": "4.8", + "free_turn": "پنجشنبه 09:00–13:00", + "hours_of_work": "شنبه تا چهارشنبه | پنجشنبه", + "active": true + } + ], + "meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 } + } } ``` +| Field | Type | Description | +|-------|------|-------------| +| `free_turn` | string | Next available appointment (e.g. `پنجشنبه 09:00–13:00`), or `نوبت آزادی موجود نیست` if the doctor has no active weekly schedule | +| `hours_of_work` | string | Working-days summary, or `برنامه کاری تنظیم نشده` when unscheduled | +| `active` | boolean | `true` only when appointments are enabled **and** the doctor has an active schedule | + +> `free_turn`/`hours_of_work`/`active` are computed from each doctor's `WeeklySchedule` (loaded in bulk by the endpoint). Without a schedule they fall back to the "not set" values. + ### Errors | Code | HTTP | Description | |------|------|-------------| diff --git a/src/Clinic/Controller/ClinicController.php b/src/Clinic/Controller/ClinicController.php index 3b967428..a6e8f4c1 100644 --- a/src/Clinic/Controller/ClinicController.php +++ b/src/Clinic/Controller/ClinicController.php @@ -8,6 +8,7 @@ use App\Clinic\Entity\Clinic; use App\Clinic\Repository\ClinicRepository; use App\Doctor\Entity\Doctor; use App\Doctor\Entity\DoctorAddress; +use App\Appointment\Repository\WeeklyScheduleRepository; use App\Doctor\Repository\DoctorAddressRepository; use App\Doctor\Repository\DoctorRepository; use App\DoctorService\Repository\DoctorServiceRepository; @@ -39,6 +40,7 @@ class ClinicController extends BaseController private readonly ProvinceRepository $provinceRepo, private readonly CityRepository $cityRepo, private readonly UserRepository $userRepo, + private readonly WeeklyScheduleRepository $scheduleRepo, private readonly FileValidatorService $fileValidator, private readonly string $projectDir, ) {} @@ -298,19 +300,34 @@ class ClinicController extends BaseController ] )] #[Route('/api/v1/clinic/doctor-list/{clinicUuid}', methods: ['GET'])] - public function doctorList(string $clinicUuid): JsonResponse + public function doctorList(string $clinicUuid, Request $request): JsonResponse { $clinic = $this->clinicRepo->findByUuid($clinicUuid); if ($clinic === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404); } + $result = $this->doctorRepo->findByClinicWithFilters((int) $clinic->getId(), $request->query->all()); + $clinicDoctors = $result['items']; + + $scheduleMap = []; + foreach ($this->scheduleRepo->findByDoctors($clinicDoctors) as $schedule) { + $scheduleMap[$schedule->getDoctor()->getId()] = $schedule; + } + $doctors = array_map( - fn(Doctor $d) => $d->toListArray(), - $clinic->getDoctors()->toArray() + fn(Doctor $d) => $d->toListArray($scheduleMap[$d->getId()] ?? null), + $clinicDoctors ); - return $this->success(['data' => $doctors]); + return $this->success([ + 'data' => $doctors, + 'meta' => [ + 'totalRecords' => $result['total'], + 'totalPages' => $result['totalPages'], + 'currentPage' => $result['page'], + ], + ]); } #[OA\Post( diff --git a/src/Doctor/Repository/DoctorRepository.php b/src/Doctor/Repository/DoctorRepository.php index 55e21a7b..c63c21c7 100644 --- a/src/Doctor/Repository/DoctorRepository.php +++ b/src/Doctor/Repository/DoctorRepository.php @@ -3,8 +3,10 @@ namespace App\Doctor\Repository; use App\Auth\Entity\User; +use App\Clinic\Entity\Clinic; use App\Doctor\Entity\Doctor; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; @@ -88,6 +90,55 @@ class DoctorRepository extends ServiceEntityRepository ]; } + public function findByClinicWithFilters(int $clinicId, array $filters): array + { + $page = max(1, (int) ($filters['page'] ?? 1)); + $limit = min(50, max(1, (int) ($filters['limit'] ?? 10))); + $sort = strtoupper($filters['sort'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC'; + + // Doctor has no inverse 'clinics' relation; the ManyToMany is owned by + // Clinic.doctors. Join Clinic and match its doctors collection to d. + $qb = $this->createQueryBuilder('d') + ->innerJoin(Clinic::class, 'c', Join::WITH, 'd MEMBER OF c.doctors') + ->leftJoin('d.specialties', 's') + ->where('c.id = :clinicId') + ->setParameter('clinicId', $clinicId) + ->distinct(); + + if (!empty($filters['specialty'])) { + $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']); + } + if (!empty($filters['gender'])) { + $qb->andWhere('d.gender = :gender')->setParameter('gender', $filters['gender']); + } + if (!empty($filters['degree'])) { + $qb->andWhere('d.degree = :degree')->setParameter('degree', $filters['degree']); + } + if (!empty($filters['name'])) { + $qb->andWhere('d.name LIKE :name')->setParameter('name', '%' . $filters['name'] . '%'); + } + if (isset($filters['active'])) { + $qb->andWhere('d.activeDoctorAppointment = :active') + ->setParameter('active', (bool) $filters['active']); + } + + $qb->orderBy('d.doctorRate', $sort); + + $total = (new Paginator($qb))->count(); + $results = $qb->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + + return [ + 'items' => $results, + 'total' => $total, + 'page' => $page, + 'limit' => $limit, + 'totalPages' => (int) ceil($total / $limit), + ]; + } + public function save(Doctor $doctor, bool $flush = true): void { $this->getEntityManager()->persist($doctor);