Files
clinicpro/.claude/prompt/location-clinic-address.md
hamed 0333b24071 feat: enhance DoctorAddress entity to support clinic addresses and types
- Added `clinic_id` and `type` fields to `DoctorAddress` entity to differentiate between personal and clinic addresses.
- Updated constructor to support creation of addresses for both doctors and clinics.
- Modified repository methods to handle new address types and added methods for counting and finding addresses by clinic.
- Implemented migration to update the database schema accordingly.
- Removed deprecated endpoint for creating addresses from clinics and updated related controller methods.
- Added new endpoints for managing clinic addresses, including CRUD operations.
- Updated frontend components to handle new address types and display accordingly.
2026-06-12 13:39:37 +03:30

15 KiB
Raw Permalink Blame History

پرامپت: مدیریت آدرس کلینیک و 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 (فعلی):

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:

// ۱. اضافه کردن 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)
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: دو حالت — برای دکتر یا برای کلینیک
// فیلدهای جدید
#[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 باید هر دو حالت را پشتیبانی کند:

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 اضافه شود:

'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:

{
  "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)

منطق:

// ۱. آدرس‌های شخصی دکتر
$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:

{
  "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 لازم:

// در 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:

pattern: ^/(api/v1/appointment-settings/available-locations/|...)

قابلیت ۶ — Frontend: DoctorDetailPage.tsx

فایل: assets/admin/pages/DoctorDetailPage.tsx

آپدیت interface:

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 بارگذاری شوند:

const locationsQ = useQuery({
  queryKey: ['available-locations', doctorUuid],
  queryFn: () => api.get<ApiResponse<AddressData[]>>(`/api/v1/appointment-settings/available-locations/${doctorUuid}`),
  enabled: !!doctorUuid,
});
const availableLocations = locationsQ.data?.data ?? [];

این query در ScheduleSection component باشد و addresses prop از parent حذف شود.

آپدیت SessionEditor — dropdown لوکیشن:

نمایش label مناسب با type:

<select value={session.location_id ?? ''}
  onChange={e => upd('location_id', e.target.value ? Number(e.target.value) : null)}>
  <option value="">انتخاب کنید</option>
  {/* آدرس‌های شخصی */}
  {addresses.filter(a => a.type === 'personal').length > 0 && (
    <optgroup label="مطب شخصی">
      {addresses.filter(a => a.type === 'personal').map(a => (
        <option key={a.id} value={a.id}>{a.name ?? a.address ?? `مطب ${a.id}`}</option>
      ))}
    </optgroup>
  )}
  {/* آدرس‌های کلینیک */}
  {addresses.filter(a => a.type === 'clinic').length > 0 && (
    <optgroup label="کلینیک‌ها">
      {addresses.filter(a => a.type === 'clinic').map(a => (
        <option key={a.id} value={a.id}>{a.clinic_name ? `${a.clinic_name}${a.name ?? a.address}` : (a.name ?? a.address ?? `کلینیک ${a.id}`)}</option>
      ))}
    </optgroup>
  )}
</select>

قابلیت ۷ — Frontend: مدیریت آدرس کلینیک در ClinicDetailPage.tsx

فایل: assets/admin/pages/ClinicDetailPage.tsx

Interface جدید:

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;
}

بارگذاری آدرس‌های کلینیک:

const addressesQ = useQuery({
  queryKey: ['clinic-addresses', uuid],
  queryFn: () => api.get<ApiResponse<ClinicAddress[]>>(`/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

تست هر مرحله

# 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}