From fa41c15bb67706f2a79db7ae9c911a16fde2f984 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 18 Jun 2026 09:06:36 +0330 Subject: [PATCH] feat(clinic): update filtering logic to use clinic address for city/state --- .../fix-clinic-filter-by-address-location.md | 116 ++++++++++++++++++ docs/api/clinic.md | 6 +- src/Clinic/Repository/ClinicRepository.php | 19 ++- 3 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 .claude/prompt/fix-clinic-filter-by-address-location.md diff --git a/.claude/prompt/fix-clinic-filter-by-address-location.md b/.claude/prompt/fix-clinic-filter-by-address-location.md new file mode 100644 index 00000000..76538ff8 --- /dev/null +++ b/.claude/prompt/fix-clinic-filter-by-address-location.md @@ -0,0 +1,116 @@ +# فیلتر کلینیک بر اساس شهر/استانِ آدرس کلینیک + +## پروژه + +`clinicpro` (Backend). تغییر فقط منطق داخلی است؛ قرارداد API (`GET /api/v1/clinics?city=&state=`) و مصرف `nobat724_front` تغییر نمی‌کند — نیازی به پرامپت همتای فرانت نیست. + +## زمینه + +معماری مکانِ کلینیک عوض شده: قبلاً شهر/استان روی خودِ `Clinic` (ستون‌های `city_id`/`province_id`) ذخیره می‌شد، اما حالا این ستون‌ها عملاً پر نمی‌شوند و **شهر/استان واقعی روی آدرس کلینیک** است — یعنی رکورد `DoctorAddress` با `clinic_id = clinic.id`، که `city_id`/`province_id` خودش را دارد. + +شواهد DB: +- `clinics`: تنها رکوردها `city_id`/`province_id` ندارند (یا قدیمی‌اند). فیلتر روی `c.cityId`/`c.provinceId` هیچ نتیجه‌ای نمی‌دهد. +- `doctor_addresses` با `type='clinic'` و `clinic_id` پر: مثلاً `clinic_id=226 → city_id=132 (یزد), province_id=100`، `clinic_id=225 → city_id=101 (تبریز), province_id=1`. + +در نتیجه `GET /api/v1/clinics?city=132&state=100` (یزد) صفر کلینیک برمی‌گرداند، هرچند کلینیک ۲۲۶ در یزد است. + +## مشکل / هدف + +`ClinicRepository::findWithFilters` باید فیلتر `city`/`state` را از طریق **آدرس کلینیک** (`DoctorAddress` با `clinic_id = clinic.id`) اعمال کند، نه ستون‌های منسوخِ `c.cityId`/`c.provinceId`. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Clinic/Repository/ClinicRepository.php` | `findWithFilters` — فیلتر مکان | +| `src/Doctor/Entity/DoctorAddress.php` | حامل مکان: `clinicId` (scalar)، `city` (ManyToOne City)، `province` (ManyToOne Province) | +| `src/Clinic/Entity/Clinic.php` | `cityId`/`provinceId` منسوخ (دیگر مبنای فیلتر نیست) | + +## وضعیت فعلی (کد واقعی) + +`ClinicRepository::findWithFilters` (مکان روی خود Clinic — نادرست): +```php +$qb = $this->createQueryBuilder('c') + ->leftJoin('c.specialties', 's') + ->distinct(); + +if (!empty($filters['state'])) { + $qb->andWhere('c.provinceId = :state')->setParameter('state', (int) $filters['state']); +} +if (!empty($filters['city'])) { + $qb->andWhere('c.cityId = :city')->setParameter('city', (int) $filters['city']); +} +if (!empty($filters['specialty'])) { + $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']); +} +$qb->orderBy('c.id', $sort); +``` + +`DoctorAddress` (مرجع — مکان واقعی): +```php +#[ORM\Column(name: 'clinic_id', type: 'integer', nullable: true)] +private ?int $clinicId = null; // scalar (نه رابطه‌ی Doctrine به Clinic) + +#[ORM\ManyToOne(targetEntity: City::class)] +#[ORM\JoinColumn(name: 'city_id', ...)] +private ?City $city = null; + +#[ORM\ManyToOne(targetEntity: Province::class)] +#[ORM\JoinColumn(name: 'province_id', ...)] +private ?Province $province = null; +``` +> چون رابطه‌ی Doctrine بین `Clinic` و `DoctorAddress` وجود ندارد (فقط scalar `clinicId`)، باید با یک join شرط‌دار (`WITH addr.clinicId = c.id`) join کرد. + +## وظایف + +### ۱. join به آدرس کلینیک و فیلتر روی city/province آدرس +`c.provinceId`/`c.cityId` را با join به `DoctorAddress` (روی `clinic_id`) و فیلتر `addr.city`/`addr.province` جایگزین کن. join فقط وقتی لازم است که `city` یا `state` در فیلترها باشد: + +```php +$qb = $this->createQueryBuilder('c') + ->leftJoin('c.specialties', 's') + ->distinct(); + +if (!empty($filters['city']) || !empty($filters['state'])) { + // DoctorAddress رابطه‌ی Doctrine به Clinic ندارد؛ با شرط clinicId join می‌کنیم + $qb->join( + \App\Doctor\Entity\DoctorAddress::class, + 'addr', + \Doctrine\ORM\Query\Expr\Join::WITH, + 'addr.clinicId = c.id' + ); + + if (!empty($filters['city'])) { + $qb->andWhere('IDENTITY(addr.city) = :city') + ->setParameter('city', (int) $filters['city']); + } + if (!empty($filters['state'])) { + $qb->andWhere('IDENTITY(addr.province) = :state') + ->setParameter('state', (int) $filters['state']); + } +} + +if (!empty($filters['specialty'])) { + $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']); +} +$qb->orderBy('c.id', $sort); +``` +- `IDENTITY(addr.city)` کلید خارجی (city_id) را بدون join اضافی به جدول `categories` می‌گیرد — سبک و دقیق. +- `distinct()` حفظ شود (یک کلینیک ممکن است چند آدرس داشته باشد). +- `Paginator` موجود روی همین `$qb` کار می‌کند؛ به‌خاطر join شرط‌دار، `distinct` باید فعال بماند تا شمارش total درست باشد. + +### ۲. مستندسازی `docs/api/clinic.md` +در بخش `GET /api/v1/clinics`، یک جمله اضافه کن که فیلتر `city`/`state` بر اساس **شهر/استانِ آدرس کلینیک** اعمال می‌شود (نه فیلد مستقیم روی کلینیک). + +## نکات مهم + +- این رفع، باگِ `Class Clinic has no field stateId` (که قبلاً به `provinceId` تغییر یافت) را هم به‌طور ریشه‌ای حل می‌کند: مکان دیگر از خود Clinic خوانده نمی‌شود. +- `DoctorAddress.clinicId` یک ستون scalar است (نه association)، پس join باید با `Join::WITH 'addr.clinicId = c.id'` باشد، نه `c.addresses`. +- ستون‌های `c.cityId`/`c.provinceId` را دست نزن (ممکن است جای دیگری استفاده شوند)؛ فقط مبنای فیلتر را عوض کن. +- پاسخ‌ها از `BaseController` (`paginated`)؛ migration لازم نیست. +- تست رفتاری (از داخل container و هاست): + - `GET /api/v1/clinics?city=132&state=100` (یزد) → باید کلینیک ۲۲۶ را برگرداند (قبلاً صفر بود). + - `GET /api/v1/clinics?city=101&state=1` (تبریز) → کلینیک ۲۲۵. + - `GET /api/v1/clinics` بدون فیلتر → همه‌ی کلینیک‌ها (join اعمال نشود). + - `?specialty=` همچنان کار کند. + - حتماً در محیط **prod** هم تست کن (`cache:clear --env=prod` سپس درخواست از `https://clinic-pro.ddev.site`) چون باگ قبلی فقط در prod ظاهر شد. diff --git a/docs/api/clinic.md b/docs/api/clinic.md index 5b5cdca1..0b57aa13 100644 --- a/docs/api/clinic.md +++ b/docs/api/clinic.md @@ -180,7 +180,11 @@ List clinics with pagination. | `page` | integer | ❌ | Default: 1 | | `limit` | integer | ❌ | Default: 20 | | `name` | string | ❌ | Search by clinic name | -| `city` | string | ❌ | Filter by city | +| `city` | integer | ❌ | City id — filters by the **clinic address's** city | +| `state` | integer | ❌ | Province id — filters by the **clinic address's** province | +| `specialty` | integer | ❌ | Specialty id | + +> `city`/`state` are matched against the clinic's address (`DoctorAddress` linked by `clinic_id`), not a field on the clinic itself. ### Response `200` ```json diff --git a/src/Clinic/Repository/ClinicRepository.php b/src/Clinic/Repository/ClinicRepository.php index 1cf65316..d5bbcff2 100644 --- a/src/Clinic/Repository/ClinicRepository.php +++ b/src/Clinic/Repository/ClinicRepository.php @@ -5,7 +5,9 @@ namespace App\Clinic\Repository; use App\Auth\Entity\User; use App\Clinic\Entity\Clinic; use App\Doctor\Entity\Doctor; +use App\Doctor\Entity\DoctorAddress; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; @@ -47,11 +49,18 @@ class ClinicRepository extends ServiceEntityRepository ->leftJoin('c.specialties', 's') ->distinct(); - if (!empty($filters['state'])) { - $qb->andWhere('c.stateId = :state')->setParameter('state', (int) $filters['state']); - } - if (!empty($filters['city'])) { - $qb->andWhere('c.cityId = :city')->setParameter('city', (int) $filters['city']); + // City/province live on the clinic's address (DoctorAddress.clinicId), + // not on the clinic itself. DoctorAddress has no Doctrine relation to + // Clinic, so join on the scalar clinicId with a WITH condition. + if (!empty($filters['city']) || !empty($filters['state'])) { + $qb->join(DoctorAddress::class, 'addr', Join::WITH, 'addr.clinicId = c.id'); + + if (!empty($filters['city'])) { + $qb->andWhere('IDENTITY(addr.city) = :city')->setParameter('city', (int) $filters['city']); + } + if (!empty($filters['state'])) { + $qb->andWhere('IDENTITY(addr.province) = :state')->setParameter('state', (int) $filters['state']); + } } if (!empty($filters['specialty'])) { $qb->andWhere('s.id = :specialty')->setParameter('specialty', (int) $filters['specialty']);