- 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.
15 KiB
پرامپت: مدیریت آدرس کلینیک و 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_iddropdown در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) - اضافه کردن
typestring (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
}
منطق:
- کلینیک را پیدا کن با
clinicUuid→ 404 اگر نبود - بررسی owner → 403 اگر دسترسی نداشت
DoctorAddress::forClinic($clinic->getId())بساز- فیلدها را ست کن (name, address, telephone, lat/lng, city, province)
- ذخیره و برگردان
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برای آدرسهایclinictype از جدول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
ترتیب اجرا
- Migration: آپدیت
doctor_addresses(type + clinic_id + nullable doctor_id) - Entity: آپدیت
DoctorAddress(static factory methods, فیلدهای جدید) - Controller: آپدیت
DoctorController(حذفfrom-clinicworkaround، آپدیتcreateAddress) - Controller: اضافه کردن CRUD آدرس کلینیک به
ClinicController - Repository:
findAvailableForDoctorوfindByUuidAndClinicبهDoctorAddressRepository - Controller: endpoint
available-locationsبهAppointmentSettingsController - Security: آپدیت
security.yamlبرایavailable-locationspublic - تست backend
- Frontend: آپدیت
DoctorDetailPage.tsx(ScheduleSection، SessionEditor) - Frontend: آپدیت
ClinicDetailPage.tsx(مدیریت آدرس) - تست frontend
- مستندسازی
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}/addressPATCH /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}