From d05c0706616bdd975acc1d484e0ea00a808e209d Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sat, 13 Jun 2026 18:30:06 +0330 Subject: [PATCH] feat: enhance doctor address management by including clinic addresses and enforcing required fields --- .../prompt/doctor-clinic-address-access.md | 214 ++++++++++++++++++ assets/admin/pages/DoctorDetailPage.tsx | 33 ++- docs/api/doctor.md | 36 ++- src/Doctor/Controller/DoctorController.php | 14 +- src/Doctor/Entity/DoctorAddress.php | 13 +- 5 files changed, 289 insertions(+), 21 deletions(-) create mode 100644 .claude/prompt/doctor-clinic-address-access.md diff --git a/.claude/prompt/doctor-clinic-address-access.md b/.claude/prompt/doctor-clinic-address-access.md new file mode 100644 index 00000000..01b75d75 --- /dev/null +++ b/.claude/prompt/doctor-clinic-address-access.md @@ -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 +// مثال: + + + + +``` + +و اضافه کردن نمایش خطا در زیر فیلدها با `formState: { errors }` از `useForm`: +```tsx +const { register, handleSubmit, watch, setValue, reset, formState: { errors } } = useForm({ + resolver: zodResolver(addrSchema), +}); +// ... +{errors.telephone &&

{errors.telephone.message}

} +{errors.address &&

{errors.address.message}

} +``` + +برای `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) diff --git a/assets/admin/pages/DoctorDetailPage.tsx b/assets/admin/pages/DoctorDetailPage.tsx index 8f6b0edb..14583b32 100644 --- a/assets/admin/pages/DoctorDetailPage.tsx +++ b/assets/admin/pages/DoctorDetailPage.tsx @@ -771,12 +771,19 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe const addrSchema = z.object({ name: z.string().optional(), - address: z.string().optional(), - telephone: z.string().max(20).optional(), + address: z.string().min(1, 'آدرس کامل اجباری است'), + telephone: z.string().min(1, 'تلفن اجباری است').max(20), province_id: z.number().nullable().optional(), city_id: z.number().nullable().optional(), latitude: z.string().optional(), longitude: z.string().optional(), +}).superRefine((data, ctx) => { + if (!data.province_id) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'استان اجباری است', path: ['province_id'] }); + } + if (!data.city_id) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'شهر اجباری است', path: ['city_id'] }); + } }); type AddrForm = z.infer; @@ -785,7 +792,7 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: { existing: AddressData | null; doctorUuid: string; onSaved: () => void; }) { - const { register, handleSubmit, watch, setValue, reset } = useForm({ + const { register, handleSubmit, watch, setValue, reset, formState: { errors } } = useForm({ resolver: zodResolver(addrSchema), }); const provinceId = watch('province_id'); @@ -864,21 +871,29 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
- + + {errors.telephone &&

{errors.telephone.message}

}
{/* Row 2: Full address */}
- +