Files
clinicpro/.claude/prompt/location-clinic-address.md
T
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

419 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# پرامپت: مدیریت آدرس کلینیک و 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<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:
```tsx
<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 جدید:
```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<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`
---
## تست هر مرحله
```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}`