# پرامپت: مدیریت آدرس کلینیک و location_id در برنامه هفتگی ## هدف مشکل فعلی: `location_id` در برنامه هفتگی (`weekly_schedules.setting`) به `doctor_addresses.id` اشاره می‌کند، اما این آدرس‌ها فقط شخصی هستند (به `doctor_id` وابسته‌اند) و هیچ ارتباطی با کلینیک ندارند. **خواسته:** ۱. کلینیک باید بتواند آدرس خودش را ثبت و مدیریت کند (آدرس کلینیک، نه آدرس شخصی دکتر) ۲. وقتی دکتری به کلینیک اضافه شد، بتواند آدرس آن کلینیک را به عنوان `location_id` در برنامه‌اش انتخاب کند ۳. آدرس‌های شخصی دکتر هم همچنان قابل استفاده باشند ۴. endpoint فعلی `POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` که یک workaround بود باید حذف شود --- ## زمینه فنی موجود ### جدول `doctor_addresses` (فعلی): ```sql id, uuid, name, address, telephone, latitude, longitude, created_at, updated_at, doctor_id INT NOT NULL, -- FK به doctors.id (CASCADE) city_id INT NULL, province_id INT NULL ``` → فقط برای آدرس‌های شخصی دکتر — clinic_id ندارد ### Entity `DoctorAddress`: - constructor: `__construct(Doctor $doctor)` — فقط برای دکتر - `toArray()` → `{ id, uuid, name, address, telephone, map, city, province }` ### Endpoint موجود (workaround که حذف می‌شود): - `POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` در `DoctorController.php` - این endpoint آدرس کلینیک را کپی می‌کند به عنوان آدرس شخصی دکتر — **باید حذف شود** ### Frontend موجود (`DoctorDetailPage.tsx`): - `addresses` از `doctor.address` می‌آید (فقط آدرس‌های شخصی دکتر) - `location_id` dropdown در `SessionEditor` از همین `addresses` ساخته می‌شود - warning «مکان مطب الزامی است» اگر `location_id = null` در session فعال باشد - interface: `AddressData { id: string; name: string | null; address: string | null; telephone: string | null; map: {...}; city: {...}; province: {...} }` ### API endpoint موجود برای آدرس دکتر: - `POST /api/v1/clinic-pro/doctor-address` — ایجاد آدرس شخصی دکتر - `GET /api/v1/clinic-pro/doctor-addresses/{doctorId}` — لیست آدرس‌های دکتر - `PATCH /api/v1/clinic-pro/doctor-address/{id}` — ویرایش - `DELETE /api/v1/clinic-pro/doctor-address/{id}` — حذف --- ## قابلیت‌ها --- ### قابلیت ۱ — Migration: تغییر ساختار `doctor_addresses` سه تغییر در یک migration file: ```php // ۱. اضافه کردن type $this->addSql("ALTER TABLE doctor_addresses ADD COLUMN type VARCHAR(10) NOT NULL DEFAULT 'personal'"); // ۲. clinic_id nullable FK $this->addSql("ALTER TABLE doctor_addresses ADD COLUMN clinic_id INT NULL"); $this->addSql("ALTER TABLE doctor_addresses ADD CONSTRAINT FK_doctor_addr_clinic FOREIGN KEY (clinic_id) REFERENCES clinics(id) ON DELETE CASCADE"); $this->addSql("CREATE INDEX idx_doctor_addr_clinic ON doctor_addresses (clinic_id)"); // ۳. doctor_id را nullable کن (آدرس‌های کلینیک doctor ندارند) $this->addSql("ALTER TABLE doctor_addresses MODIFY COLUMN doctor_id INT NULL"); // قانون: حداقل یکی از doctor_id یا clinic_id باید مقدار داشته باشد $this->addSql("ALTER TABLE doctor_addresses ADD CONSTRAINT chk_addr_owner CHECK (doctor_id IS NOT NULL OR clinic_id IS NOT NULL)"); ``` **مقادیر `type`:** - `personal` — آدرس شخصی دکتر (doctor_id NOT NULL, clinic_id NULL) - `clinic` — آدرس کلینیک (clinic_id NOT NULL, doctor_id NULL) ```bash ddev exec php bin/console doctrine:migrations:migrate --no-interaction ``` --- ### قابلیت ۲ — آپدیت Entity `DoctorAddress` **فایل:** `src/Doctor/Entity/DoctorAddress.php` تغییرات: - `doctor_id` → nullable (`?int $doctorId` + join nullable) - اضافه کردن `clinic_id` (nullable int) - اضافه کردن `type` string (`personal` | `clinic`) - constructor: دو حالت — برای دکتر یا برای کلینیک ```php // فیلدهای جدید #[ORM\Column(type: 'string', length: 10)] private string $type = 'personal'; #[ORM\Column(name: 'clinic_id', type: 'integer', nullable: true)] private ?int $clinicId = null; // doctor nullable شود #[ORM\ManyToOne(targetEntity: Doctor::class, inversedBy: 'addresses')] #[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] private ?Doctor $doctor = null; ``` Constructor باید هر دو حالت را پشتیبانی کند: ```php public static function forDoctor(Doctor $doctor): self { $a = new self(); $a->type = 'personal'; $a->doctor = $doctor; return $a; } public static function forClinic(int $clinicId): self { $a = new self(); $a->type = 'clinic'; $a->clinicId = $clinicId; return $a; } ``` `toArray()` آپدیت شود: فیلد `type` اضافه شود: ```php 'type' => $this->type, 'clinic_id' => $this->clinicId, ``` > ⚠️ Constructor قدیمی `__construct(Doctor $doctor)` تغییر می‌کند. همه جاهایی که `new DoctorAddress($doctor)` دارند باید به `DoctorAddress::forDoctor($doctor)` تغییر کنند: > - `src/Doctor/Controller/DoctorController.php` متد `createAddress` و `createAddressFromClinic` --- ### قابلیت ۳ — حذف endpoint قدیمی و آپدیت DoctorController **فایل:** `src/Doctor/Controller/DoctorController.php` ۱. متد `createAddressFromClinic` (route: `POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}`) را کامل حذف کن. ۲. در `createAddress`: `new DoctorAddress($doctor)` → `DoctorAddress::forDoctor($doctor)` ۳. در `updateAddress` (PATCH): چک کن آدرس از نوع `personal` باشد — آدرس‌های `clinic` type را فقط صاحب کلینیک می‌تواند ویرایش کند (در قابلیت ۴ هندل می‌شود) --- ### قابلیت ۴ — CRUD آدرس کلینیک **فایل:** `src/Clinic/Controller/ClinicController.php` #### `POST /api/v1/clinic/{clinicUuid}/address` — ایجاد آدرس کلینیک Auth: صاحب کلینیک یا admin Request body: ```json { "name": "شعبه مرکزی", "address": "تهران، خیابان ولیعصر...", "telephone": "02112345678", "latitude": 35.699, "longitude": 51.337, "city_id": 123, "province_id": 7 } ``` منطق: 1. کلینیک را پیدا کن با `clinicUuid` → 404 اگر نبود 2. بررسی owner → 403 اگر دسترسی نداشت 3. `DoctorAddress::forClinic($clinic->getId())` بساز 4. فیلدها را ست کن (name, address, telephone, lat/lng, city, province) 5. ذخیره و برگردان Response: `{ success, data: { id, uuid, type: 'clinic', name, address, ... } }` #### `PATCH /api/v1/clinic/{clinicUuid}/address/{addressUuid}` — ویرایش - پیدا کردن `DoctorAddress` با `uuid` و `clinic_id = clinic.id` - بررسی ownership - آپدیت فیلدها #### `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}` — حذف - اگر فقط یک آدرس مانده → 409 با پیام «کلینیک باید حداقل یک آدرس داشته باشد» - در غیر این صورت حذف کن #### `GET /api/v1/clinic/{clinicUuid}/addresses` — لیست آدرس‌ها - برگرداندن همه `doctor_addresses` که `clinic_id = clinic.id` - Auth: public (برای نمایش در صفحه کلینیک برای بیماران) > ⚠️ برای find با uuid نیاز به متد `findByUuidAndClinic(string $uuid, int $clinicId)` در `DoctorAddressRepository` است. --- ### قابلیت ۵ — Endpoint آدرس‌های در دسترس دکتر **فایل:** `src/Appointment/Controller/AppointmentSettingsController.php` **Route:** `GET /api/v1/appointment-settings/available-locations/{doctorUuid}` **Auth:** IS_AUTHENTICATED_FULLY (دکتر یا admin) **منطق:** ```php // ۱. آدرس‌های شخصی دکتر $personal = $addressRepo->findBy(['doctor' => $doctor, 'type' => 'personal']); // ۲. آدرس‌های کلینیک‌هایی که این دکتر عضوشان است $clinicIds = array_map(fn(Clinic $c) => $c->getId(), $clinicRepo->findByDoctor($doctor)); $clinicAddresses = $addressRepo->findBy(['clinicId' => $clinicIds, 'type' => 'clinic']); // ۳. ادغام با label مناسب ``` Response: ```json { "success": true, "data": [ { "id": "5", "uuid": "...", "type": "personal", "name": "مطب شخصی", "address": "...", "clinic_name": null }, { "id": "12", "uuid": "...", "type": "clinic", "name": "شعبه مرکزی", "address": "...", "clinic_name": "کلینیک نور" } ] } ``` > `clinic_name` برای آدرس‌های `clinic` type از جدول `clinics` می‌آید — با یک query join بگیر. **Repository method لازم:** ```php // در DoctorAddressRepository public function findAvailableForDoctor(Doctor $doctor, array $clinicIds): array { $qb = $this->createQueryBuilder('a'); return $qb ->where('(a.doctor = :doctor AND a.type = :personal)') ->orWhere('(a.clinicId IN (:clinicIds) AND a.type = :clinic)') ->setParameter('doctor', $doctor) ->setParameter('personal', 'personal') ->setParameter('clinicIds', empty($clinicIds) ? [0] : $clinicIds) ->setParameter('clinic', 'clinic') ->getQuery() ->getResult(); } ``` **Security:** اضافه کردن به `public_endpoints` در `security.yaml`: ```yaml pattern: ^/(api/v1/appointment-settings/available-locations/|...) ``` --- ### قابلیت ۶ — Frontend: `DoctorDetailPage.tsx` **فایل:** `assets/admin/pages/DoctorDetailPage.tsx` #### آپدیت interface: ```ts interface AddressData { id: string; uuid: string; type: 'personal' | 'clinic'; name: string | null; address: string | null; telephone: string | null; map: { latitude: string | null; longitude: string | null }; city: { id: string; name: string } | null; province: { id: string; name: string } | null; clinic_name: string | null; // جدید } ``` #### آپدیت بارگذاری آدرس‌ها در `ScheduleSection`: فعلاً: `addresses = doctor.address ?? []` (فقط شخصی) جدید: آدرس‌ها از `available-locations` endpoint بارگذاری شوند: ```ts const locationsQ = useQuery({ queryKey: ['available-locations', doctorUuid], queryFn: () => api.get>(`/api/v1/appointment-settings/available-locations/${doctorUuid}`), enabled: !!doctorUuid, }); const availableLocations = locationsQ.data?.data ?? []; ``` این query در `ScheduleSection` component باشد و `addresses` prop از parent حذف شود. #### آپدیت `SessionEditor` — dropdown لوکیشن: نمایش label مناسب با type: ```tsx ``` --- ### قابلیت ۷ — Frontend: مدیریت آدرس کلینیک در `ClinicDetailPage.tsx` **فایل:** `assets/admin/pages/ClinicDetailPage.tsx` #### Interface جدید: ```ts interface ClinicAddress { id: string; uuid: string; name: string | null; address: string | null; telephone: string | null; map: { latitude: string | null; longitude: string | null }; city: { id: string; name: string } | null; province: { id: string; name: string } | null; } ``` #### بارگذاری آدرس‌های کلینیک: ```ts const addressesQ = useQuery({ queryKey: ['clinic-addresses', uuid], queryFn: () => api.get>(`/api/v1/clinic/${uuid}/addresses`), }); const clinicAddresses = addressesQ.data?.data ?? []; ``` #### UI (فقط برای clinic owner): - سکشن «آدرس‌های کلینیک» با دکمه «افزودن آدرس» - هر آدرس: نام + آدرس + تلفن + دکمه‌های ویرایش/حذف - اگر آدرسی ندارد: warning «کلینیک هنوز آدرسی ثبت نکرده — دکتران نمی‌توانند این کلینیک را به عنوان لوکیشن انتخاب کنند» - Modal برای add/edit با فیلدهای: name, address, telephone, city_id, province_id - mutation‌های `addAddressMutation`, `editAddressMutation`, `deleteAddressMutation` --- ## ترتیب اجرا 1. Migration: آپدیت `doctor_addresses` (type + clinic_id + nullable doctor_id) 2. Entity: آپدیت `DoctorAddress` (static factory methods, فیلدهای جدید) 3. Controller: آپدیت `DoctorController` (حذف `from-clinic` workaround، آپدیت `createAddress`) 4. Controller: اضافه کردن CRUD آدرس کلینیک به `ClinicController` 5. Repository: `findAvailableForDoctor` و `findByUuidAndClinic` به `DoctorAddressRepository` 6. Controller: endpoint `available-locations` به `AppointmentSettingsController` 7. Security: آپدیت `security.yaml` برای `available-locations` public 8. تست backend 9. Frontend: آپدیت `DoctorDetailPage.tsx` (ScheduleSection، SessionEditor) 10. Frontend: آپدیت `ClinicDetailPage.tsx` (مدیریت آدرس) 11. تست frontend 12. مستندسازی `docs/api/clinic.md` و `docs/api/appointment-settings.md` --- ## تست هر مرحله ```bash # migration ddev exec php bin/console doctrine:migrations:migrate --no-interaction # syntax ddev exec php -l src/Doctor/Entity/DoctorAddress.php ddev exec php -l src/Doctor/Controller/DoctorController.php ddev exec php -l src/Clinic/Controller/ClinicController.php ddev exec php -l src/Appointment/Controller/AppointmentSettingsController.php # cache + routes ddev exec php bin/console cache:clear ddev exec php bin/console debug:router | grep -E "clinic.*address|available-location" # frontend ddev exec yarn dev ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head -30 ``` --- ## مستندسازی ### `docs/api/clinic.md`: - `POST /api/v1/clinic/{clinicUuid}/address` - `PATCH /api/v1/clinic/{clinicUuid}/address/{addressUuid}` - `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}` - `GET /api/v1/clinic/{clinicUuid}/addresses` - حذف endpoint: `POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` ### `docs/api/appointment-settings.md`: - `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`