feat: enhance doctor address management by including clinic addresses and enforcing required fields
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# دسترسی پزشک به آدرسهای کلینیکهای عضو + اجباری کردن فیلدهای آدرس
|
||||
|
||||
## زمینه
|
||||
|
||||
وقتی یک پزشک عضو یک کلینیک میشود، آدرسهای آن کلینیک باید در پروفایل پزشک (بخش «آدرسهای مطب») نمایش داده شود. این آدرسها با `type: 'clinic'` در entity `DoctorAddress` ذخیره میشوند و `clinic_id` دارند، اما در پاسخ API فعلی `clinic_name` برگردانده نمیشود در حالی که frontend آن را از `a.clinic_name` میخواند.
|
||||
|
||||
همچنین پزشک باید بتواند آدرسهای کلینیکهایی که عضو آنهاست را ببیند — در حال حاضر `GET /api/v1/clinic-pro/doctor-addresses/{doctorId}` فقط آدرسهای مستقیم Doctor entity را برمیگرداند و آدرسهای کلینیکهای وابسته را شامل نمیشود.
|
||||
|
||||
علاوه بر این، در `AddressModal` (افزودن آدرس جدید در پروفایل پزشک)، فیلدهای `telephone`، `address`، `province_id` و `city_id` اختیاری هستند و باید اجباری شوند.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
سه هدف مجزا:
|
||||
|
||||
**۱.** آدرسهای کلینیکهای عضو پزشک باید در لیست آدرسهای پزشک نمایش داده شوند
|
||||
**۲.** `DoctorAddress::toArray()` باید `clinic_name` را نیز برگرداند تا frontend بتواند آن را نشان دهد
|
||||
**۳.** در `AddressModal`، فیلدهای تلفن، آدرس کامل، شهر و استان باید اجباری (required) باشند
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Doctor/Entity/DoctorAddress.php` | entity آدرس — متد `toArray()` باید `clinic_name` اضافه کند |
|
||||
| `src/Doctor/Controller/DoctorController.php` | `listAddresses()` — خط 671 — باید آدرسهای کلینیکها را هم شامل شود |
|
||||
| `src/Clinic/Repository/ClinicRepository.php` | دارای `findByDoctor(Doctor $doctor): array` |
|
||||
| `src/Clinic/Controller/ClinicController.php` | `listAddresses()` — خط 544 — PUBLIC، آدرسهای کلینیک را برمیگرداند |
|
||||
| `assets/admin/pages/DoctorDetailPage.tsx` | `addrSchema` در خط 772 — `AddressModal` — فرم افزودن آدرس |
|
||||
| `docs/api/doctor.md` | مستندات endpoint پزشک |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
### Backend — DoctorAddress::toArray() (فایل: `src/Doctor/Entity/DoctorAddress.php`, خط 112)
|
||||
|
||||
```php
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $this->id,
|
||||
'uuid' => $this->uuid,
|
||||
'type' => $this->type,
|
||||
'clinic_id' => $this->clinicId, // ← فقط ID، بدون نام
|
||||
'name' => $this->name,
|
||||
// ...
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Backend — listAddresses در DoctorController (خط 671)
|
||||
|
||||
```php
|
||||
#[Route('/api/v1/clinic-pro/doctor-addresses/{doctorId}', methods: ['GET'])]
|
||||
public function listAddresses(int $doctorId): JsonResponse
|
||||
{
|
||||
$doctor = $this->doctorRepo->find($doctorId);
|
||||
// ...
|
||||
$addresses = array_map(fn(DoctorAddress $a) => $a->toArray(), $doctor->getAddresses()->toArray());
|
||||
// ← فقط آدرسهای مستقیم Doctor entity — آدرسهای کلینیکهای عضو شامل نمیشود
|
||||
return $this->success(['data' => $addresses]);
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend — addrSchema در DoctorDetailPage.tsx (خط 772)
|
||||
|
||||
```tsx
|
||||
const addrSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
address: z.string().optional(), // ← باید required باشد
|
||||
telephone: z.string().max(20).optional(), // ← باید required باشد
|
||||
province_id: z.number().nullable().optional(), // ← باید required باشد
|
||||
city_id: z.number().nullable().optional(), // ← باید required باشد
|
||||
latitude: z.string().optional(),
|
||||
longitude: z.string().optional(),
|
||||
});
|
||||
```
|
||||
|
||||
### Frontend — SessionEditor (خط 1082-1085)
|
||||
|
||||
```tsx
|
||||
...addresses.filter(a => a.type === 'clinic').map(a => ({
|
||||
label: `🏨 ${a.clinic_name ? `${a.clinic_name}${a.name ? ` — ${a.name}` : ''}` : (a.name ?? a.address ?? `کلینیک ${a.id}`)}`,
|
||||
}))
|
||||
// ← a.clinic_name را استفاده میکند اما toArray() این فیلد را برنمیگرداند
|
||||
```
|
||||
|
||||
## وضعیت ClinicRepository
|
||||
|
||||
```php
|
||||
// src/Clinic/Repository/ClinicRepository.php — خط 25
|
||||
public function findByDoctor(Doctor $doctor): array
|
||||
{
|
||||
return $this->createQueryBuilder('c')
|
||||
->innerJoin('c.doctors', 'd')
|
||||
->where('d.id = :doctorId')
|
||||
->setParameter('doctorId', $doctor->getId())
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
```
|
||||
|
||||
متد `findByDoctor()` وجود دارد — از آن استفاده کن.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. Backend — اضافه کردن `clinic_name` به `DoctorAddress::toArray()`
|
||||
|
||||
در `src/Doctor/Entity/DoctorAddress.php`، `DoctorAddress` entity دارای فیلد `clinicId` (integer) است اما ارتباط مستقیم به entity `Clinic` ندارد. بنابراین `clinic_name` باید از طریق injection خارجی به toArray اضافه شود.
|
||||
|
||||
**روش پیشنهادی:** متد `toArray()` را با پارامتر اختیاری گسترش بده:
|
||||
|
||||
```php
|
||||
public function toArray(?string $clinicName = null): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $this->id,
|
||||
'uuid' => $this->uuid,
|
||||
'type' => $this->type,
|
||||
'clinic_id' => $this->clinicId,
|
||||
'clinic_name' => $clinicName, // ← جدید
|
||||
'name' => $this->name,
|
||||
// ... بقیه فیلدها بدون تغییر
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### ۲. Backend — آدرسهای کلینیکهای عضو را در `listAddresses` شامل کن
|
||||
|
||||
در `src/Doctor/Controller/DoctorController.php`، متد `listAddresses` (خط 671):
|
||||
|
||||
1. `ClinicRepository` را به constructor اضافه کن (اگر از قبل وجود ندارد، بررسی کن)
|
||||
2. آدرسهای کلینیکهای عضو پزشک را دریافت کن
|
||||
3. آنها را به لیست آدرسهای پزشک اضافه کن
|
||||
|
||||
```php
|
||||
public function listAddresses(int $doctorId): JsonResponse
|
||||
{
|
||||
$doctor = $this->doctorRepo->find($doctorId);
|
||||
if ($doctor === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
|
||||
// آدرسهای شخصی پزشک
|
||||
$personalAddresses = array_map(
|
||||
fn(DoctorAddress $a) => $a->toArray(),
|
||||
$doctor->getAddresses()->toArray()
|
||||
);
|
||||
|
||||
// آدرسهای کلینیکهایی که پزشک عضو آنهاست
|
||||
$clinicAddresses = [];
|
||||
$clinics = $this->clinicRepo->findByDoctor($doctor);
|
||||
foreach ($clinics as $clinic) {
|
||||
$clinicAddrs = $this->addressRepo->findBy(['clinicId' => $clinic->getId()]);
|
||||
foreach ($clinicAddrs as $addr) {
|
||||
$clinicAddresses[] = $addr->toArray($clinic->getName());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success(['data' => array_merge($personalAddresses, $clinicAddresses)]);
|
||||
}
|
||||
```
|
||||
|
||||
**نکته:** `$this->addressRepo` باید از نوع `DoctorAddressRepository` باشد (همان که در ClinicController استفاده میشود). بررسی کن که DoctorController این repo را دارد یا نه — اگر ندارد به constructor اضافه کن.
|
||||
|
||||
**نکته:** `$this->clinicRepo` از نوع `ClinicRepository` — بررسی کن DoctorController این dependency را دارد.
|
||||
|
||||
### ۳. Frontend — اجباری کردن فیلدها در `addrSchema`
|
||||
|
||||
در `assets/admin/pages/DoctorDetailPage.tsx`، خط 772، schema را به این شکل تغییر بده:
|
||||
|
||||
```tsx
|
||||
const addrSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
address: z.string().min(1, 'آدرس کامل اجباری است'),
|
||||
telephone: z.string().min(1, 'تلفن اجباری است').max(20),
|
||||
province_id: z.number({ required_error: 'استان اجباری است' }),
|
||||
city_id: z.number({ required_error: 'شهر اجباری است' }),
|
||||
latitude: z.string().optional(),
|
||||
longitude: z.string().optional(),
|
||||
});
|
||||
```
|
||||
|
||||
همچنین لیبل فیلدهای اجباری در form باید علامت ستاره (`*`) داشته باشند — فیلدهای `address`، `telephone`، `استان`، `شهر`.
|
||||
|
||||
در JSX، لیبلهای مربوطه را به این شکل تغییر بده:
|
||||
```tsx
|
||||
// مثال:
|
||||
<label>تلفن <span className="text-red-500">*</span></label>
|
||||
<label>آدرس کامل <span className="text-red-500">*</span></label>
|
||||
<label>استان <span className="text-red-500">*</span></label>
|
||||
<label>شهر <span className="text-red-500">*</span></label>
|
||||
```
|
||||
|
||||
و اضافه کردن نمایش خطا در زیر فیلدها با `formState: { errors }` از `useForm`:
|
||||
```tsx
|
||||
const { register, handleSubmit, watch, setValue, reset, formState: { errors } } = useForm<AddrForm>({
|
||||
resolver: zodResolver(addrSchema),
|
||||
});
|
||||
// ...
|
||||
{errors.telephone && <p className="text-xs text-red-500 mt-1">{errors.telephone.message}</p>}
|
||||
{errors.address && <p className="text-xs text-red-500 mt-1">{errors.address.message}</p>}
|
||||
```
|
||||
|
||||
برای `province_id` و `city_id` که از `SearchableSelect` استفاده میکنند، error در زیر select نمایش بده.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **Migration لازم نیست** — هیچ schema تغییر نمیکند، فقط response format و validation
|
||||
- **`DoctorAddress::toArray(?string $clinicName = null)`** — پارامتر optional است، همه فراخوانیهای فعلی بدون تغییر کار میکنند
|
||||
- **DoctorController constructor** — بررسی کن آیا `ClinicRepository` و `DoctorAddressRepository` از قبل inject شدهاند
|
||||
- **ClinicController** دارای `$this->addressRepo` از نوع `DoctorAddressRepository` — در DoctorController باید نام variable یکسان باشد (بررسی کن)
|
||||
- **frontend** در خط 1084: `a.clinic_name` — بعد از اضافه شدن این فیلد به response، این بخش کار میکند
|
||||
- `GET /api/v1/clinic-pro/doctor-addresses/{doctorId}` PUBLIC است — نیازی به auth guard جدید نیست
|
||||
- بعد از تغییر controller: `ddev exec php bin/console cache:clear`
|
||||
- بعد از تغییر frontend: `ddev exec yarn dev` و TypeScript check
|
||||
- مستندات: `docs/api/doctor.md` را با تغییرات endpoint `listAddresses` بهروزرسانی کن (اضافه شدن clinic addresses به response)
|
||||
Reference in New Issue
Block a user