feat(clinic): update filtering logic to use clinic address for city/state
This commit is contained in:
@@ -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=<id>` همچنان کار کند.
|
||||
- حتماً در محیط **prod** هم تست کن (`cache:clear --env=prod` سپس درخواست از `https://clinic-pro.ddev.site`) چون باگ قبلی فقط در prod ظاهر شد.
|
||||
+5
-1
@@ -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
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user