Three views become two. The resource lanes were a separate tab, which meant reading a doctor's free hour on one screen and the laser's on another and matching them by eye — while in the resource-first model it is the device and the room that decide whether that hour is really free. They now sit under the same "زمانبندی" view, below the doctor's slots. Each lane says how much of its shift is still free, and that number respects capacity: a minute counts as busy only once the overlapping bookings reach the resource's capacity, so a three-bed room with two appointments is still open. Treating it otherwise would silently turn every multi-capacity resource into a single-capacity one. ResourceFreeTimeCalculator does the sweep and carries nine cases of its own. only_bookable=1 keeps resources with no service offering out of the view; they could only ever render an empty lane. On the seeded clinic that is five resources down to two. Two ruler defects the screenshot caught: hours rendered in Latin digits, and the last label was half-clipped by the container so 21 read as 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
625 lines
25 KiB
Markdown
625 lines
25 KiB
Markdown
# Doctor API
|
||
|
||
> **`name` در برابر `display_name`:** `name` نامِ خام است (بدون عنوان — برای فرم ویرایش).
|
||
> `display_name` نامِ آمادهٔ نمایش با پیشوند «دکتر» است که بکاند تولید میکند
|
||
> (`PersianText::withDoctorTitle`) تا همهٔ کلاینتها یکسان و بدون «دکتر دکتر …» نشان دهند؛
|
||
> در لیست و جزئیات پزشک هر دو حاضرند. برای نمایش از `display_name`، برای ویرایش از `name` استفاده کنید.
|
||
|
||
> فیلد `owner_status` (`claimed` | `unclaimed` | `pending_transfer`) به خروجی لیست و جزئیات پزشک
|
||
> اضافه شده است — پروفایل `unclaimed` (ایمپورت نظام پزشکی) در سایت دکمهٔ «تصاحب پروفایل» میگیرد
|
||
> (`docs/api/doctor-claim.md`) و نوبتدهی آنلاینش غیرفعال است.
|
||
>
|
||
> **امتیاز فقط برای پروفایل `claimed` منتشر میشود.** برای هر `owner_status` غیر از `claimed`
|
||
> (یعنی `unclaimed` / `pending_transfer`) فیلدهای `point` و `satisfaction` مقدار `null` برمیگردند —
|
||
> مقدار پیشفرض انتیتی (`3.5` / `60`) نشتی نمیکند تا امتیاز جعلی/`AggregateRating` جعلی ساخته نشود.
|
||
> بعد از تصاحب و claimed شدن، امتیاز واقعی بهصورت خودکار برمیگردد.
|
||
|
||
> **Prefix:** `/api/v1/doctor`, `/api/v1/doctors`, `/api/v1/clinic-pro/doctor-address*`
|
||
>
|
||
> Numeric path params on the address routes (`doctor-address/{id}`, `doctor-addresses/{doctorId}`) require `\d+`; a non-numeric value returns a clean `404` instead of a `500`.
|
||
|
||
---
|
||
|
||
## POST `/api/v1/doctor`
|
||
|
||
Create a doctor profile for the authenticated user.
|
||
|
||
**Permission:** `AUTH` — any authenticated user
|
||
|
||
### Request Body (`application/json`)
|
||
```json
|
||
{
|
||
"title": "علی احمدی",
|
||
"gender": "male",
|
||
"medical_system_code": "12345",
|
||
"degree": "متخصص",
|
||
"info": "توضیحات درباره پزشک",
|
||
"specialties": [1, 2],
|
||
"doctor_services": [3, 4]
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `title` | string | ✅ | نام پزشک **بدون** عنوان. پیشوند «دکتر» سمت سرور با `PersianText::stripDoctorTitle()` حذف میشود؛ نمایش عنوان کار لایهٔ UI است. |
|
||
| `gender` | string | ❌ | `"male"` or `"female"` |
|
||
| `medical_system_code` | string | ❌ | Nظام پزشکی code |
|
||
| `degree` | string | ❌ | Academic degree |
|
||
| `info` | string | ❌ | Bio/description |
|
||
| `specialties` | integer[] | ❌ | Array of specialty IDs. تخصصها درختیاند: هر شناسهٔ فرزند سمت سرور با تمام والدهایش تا ریشه گسترش مییابد، پس پاسخ ممکن است تخصصهای بیشتری از ورودی داشته باشد (مثلاً ارسال «گوارش و کبد» → ذخیرهٔ «داخلی» + «گوارش و کبد»). شناسههای ناموجود نادیده گرفته میشوند. |
|
||
| `doctor_services` | integer[] | ❌ | Array of doctor service IDs |
|
||
| `activity_time` | integer | ❌ | Unix timestamp (ثانیه) تاریخ شروع فعالیت؛ مبنای محاسبهٔ `experience` (سال تجربه) در پاسخ |
|
||
|
||
### Response `201`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||
"title": "علی احمدی",
|
||
"gender": "male",
|
||
"medical_system_code": "12345",
|
||
"degree": "متخصص",
|
||
"info": "...",
|
||
"doctor_rate": null,
|
||
"active_doctor_appointment": false,
|
||
"specialties": [],
|
||
"doctor_services": [],
|
||
"created_at": 1717000000
|
||
}
|
||
}
|
||
```
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_AUTH_001` | 401 | Missing or invalid token |
|
||
| `ERR_CONFLICT_001` | 409 | Doctor profile already exists for this user |
|
||
| `ERR_VALIDATION_002` | 422 | Missing required field |
|
||
| `ERR_VALIDATION_001` | 422 | نام پزشک شمارهتلفن یا مقدار آزمایشی است |
|
||
|
||
---
|
||
|
||
## GET `/api/v1/doctor/{uuid}`
|
||
|
||
Get doctor detail with clinics.
|
||
|
||
**Permission:** `PUBLIC`
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `uuid` | string (UUID) | Doctor UUID |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"data": {
|
||
"uuid": "550e8400-...",
|
||
"name": "علی احمدی",
|
||
"display_name": "دکتر علی احمدی",
|
||
"gender": "man",
|
||
"medical_system_code": "12345",
|
||
"degree": "specialist",
|
||
"detail": "...",
|
||
"img": [],
|
||
"social_media": {
|
||
"instagram": "https://instagram.com/dr.example",
|
||
"telegram": "https://t.me/dr_example",
|
||
"aparat": null,
|
||
"youtube": null,
|
||
"linkedin": null
|
||
},
|
||
"satisfaction": "60",
|
||
"point": "3.5",
|
||
"free_turn": "دوشنبه 09:00–13:00",
|
||
"hours_of_work": "شنبه: 09:00–13:00 و 14:00–18:00 | یکشنبه: 09:00–13:00",
|
||
"active": true,
|
||
"owner_status": "claimed",
|
||
"specialties": [{ "uuid": "...", "id": "1", "name": "قلب و عروق", "parent_id": null }],
|
||
"expertise": [{ "uuid": "...", "id": "3", "name": "نوار قلب" }],
|
||
"address": [],
|
||
"state": [],
|
||
"city": [],
|
||
"clinics": [{ "uuid": "...", "name": "کلینیک الوند", "address": "...", "telephone": "..." }],
|
||
"representation": { "id": 12, "uuid": "9c1...", "full_name": "علی محمدی" }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
|
||
>
|
||
> ℹ️ `representation` نمایندهی مالکِ پزشک است؛ برای پزشکِ بدون نماینده `null`.
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
|
||
|
||
---
|
||
|
||
## GET `/api/v1/clinic/my-doctor/{doctorUuid}`
|
||
|
||
Get doctor detail for clinic owner — only doctors who are members of the authenticated clinic.
|
||
|
||
**Permission:** `ROLE_CLINIC`
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `doctorUuid` | string (UUID) | Doctor UUID |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"data": {
|
||
"uuid": "...",
|
||
"title": "علی احمدی",
|
||
"specialties": [...],
|
||
"clinics": [{ "uuid": "...", "name": "کلینیک نور", "address": "...", "telephone": "..." }]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
|
||
|
||
> **Side note:** Returns only the authenticated clinic's data in the `clinics` array (not all clinics of the doctor).
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_VALIDATION_002` | 404 | Clinic or doctor not found |
|
||
| `ERR_AUTH_006` | 403 | Doctor is not a member of this clinic |
|
||
|
||
---
|
||
|
||
### Schedule Fields Notes
|
||
|
||
**تجمیع همهٔ برنامهها (2026-07):** این فیلدها روی **همهٔ** برنامههای هفتگی پزشک محاسبه
|
||
میشوند — برنامهٔ مطب شخصی (`clinic_id IS NULL`) بهعلاوهٔ یک برنامه به ازای هر کلینیک.
|
||
پزشکی که برنامهٔ شخصی خالی/خاموش ولی برنامهٔ کلینیکیِ فعال دارد، `active=true` میگیرد؛
|
||
برنامهٔ یک محیط هرگز محیط دیگر را نمیپوشاند.
|
||
|
||
| Field | When at least one schedule is bookable | When none |
|
||
|-------|---------------------------------------|-----------|
|
||
| `free_turn` | نزدیکترین روز/ساعت کاری از امروز، بین همهٔ برنامههای روشن (مثلاً «دوشنبه ۹:۰۰–۱۳:۰۰») | «نوبت آزادی موجود نیست» |
|
||
| `hours_of_work` | خلاصه ساعتهای همان برنامهای که `free_turn` را داده (ساعتهای دو محل با هم ترکیب نمیشوند) | «برنامه کاری تنظیم نشده» |
|
||
| `active` | `activeDoctorAppointment && (∃ schedule: online_booking_enabled && has_active_sessions)` | `false` — نوبتدهی غیرفعال |
|
||
|
||
> **نوبتدهی آنلاین غیرفعال:** اگر `meta.online_booking_enabled` در **همهٔ** برنامههای پزشک
|
||
> `false` باشد، `free_turn` برابر `"نوبتدهی آنلاین غیرفعال است"` و `active` برابر `false`
|
||
> برمیگردد؛ `hours_of_work` در صورت وجود برنامه حفظ میشود. تا وقتی حتی یک برنامه روشن و
|
||
> دارای روز فعال باشد، همان مبنا قرار میگیرد.
|
||
>
|
||
> چنین پزشکی (همه برنامهها خاموش، ولی فلگ `active_doctor_appointment` **روشن**) در لیست
|
||
> عمومی `GET /api/v1/doctors` **نمایش داده میشود** — چون هنوز فعال است — ولی با
|
||
> `bookableRank` پایینتر از پزشکان دارای نوبت مرتب میشود و تنها با `active=1` از نتایج حذف
|
||
> میگردد.
|
||
>
|
||
> **پزشک غیرفعال (`active_doctor_appointment` خاموش):** با غیرفعالکردن پزشک از پنل ادمین،
|
||
> او دیگر در لیست عمومی `GET /api/v1/doctors` ظاهر نمیشود (پیشفرض این اندپوینت پزشکان
|
||
> غیرفعال را کنار میگذارد). صفحهٔ تکی `GET /api/v1/doctor/{slug}` همچنان پاسخ میدهد
|
||
> (ادمین/کلینیک از همین اندپوینت برای مشاهده/ویرایش استفاده میکنند) و در بدنهٔ پاسخ فیلد
|
||
> خام `is_active` (`= active_doctor_appointment`، مستقل از داشتن برنامه) را برمیگرداند؛
|
||
> کلاینت عمومی مثل nobat724 با `is_active === false` صفحهٔ پزشک را `404` میکند. این با
|
||
> فیلد `active` (که `active_doctor_appointment && has_schedule` است) فرق دارد.
|
||
|
||
---
|
||
|
||
## GET `/api/v1/doctors`
|
||
|
||
List doctors with pagination and filters.
|
||
|
||
**Permission:** `PUBLIC`
|
||
|
||
### Query Parameters
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `page` | integer | ❌ | Default: 1 |
|
||
| `limit` | integer | ❌ | Default: 10. **حداکثر ۵۰** — مقادیر بزرگتر بیصدا به ۵۰ کاهش مییابند. مقدار واقعاً اعمالشده در `meta.limit` برمیگردد؛ برای پیمایش کامل به `meta.totalPages` تکیه کن، نه به «تعداد آیتم کمتر از limit درخواستی» |
|
||
| `search` | string | ❌ | Search in title |
|
||
| `specialty_id` | integer | ❌ | Filter by specialty ID |
|
||
| `city_id` | integer | ❌ | Filter by city ID — شامل دکترهایی که آدرس شخصیشان (`doctor_addresses.city_id`, با `doctor_id` مقداردار) در آن شهر است یا از طریق کلینیکی که آدرس آن در آن شهر است (`doctor_addresses.clinic_id`) |
|
||
| `state_id` | integer | ❌ | Filter by province ID — بر اساس آدرس شخصی پزشک (`doctor_addresses.province_id`) یا آدرس کلینیک |
|
||
| `domain` | string | ❌ | دامنهی سایتِ درخواستکننده. اگر دامنهی یک **نماینده سراسری** باشد، فقط پزشکانِ همان نماینده برمیگردند و `city_id`/`state_id` نادیده گرفته میشوند؛ دامنه شهری/ناشناخته اثری ندارد |
|
||
| `gender` | string | ❌ | `man` یا `woman` |
|
||
| `degree` | string | ❌ | `expert`, `general`, `specialist`, `subspecialistplus` |
|
||
| `name` | string | ❌ | جستجوی `LIKE` روی نام پزشک |
|
||
| `sort` | string | ❌ | `ASC` یا `DESC` (پیشفرض `DESC`) — مرتبسازی ثانویه بر اساس `doctorRate` |
|
||
| `active` | `0`/`1` | ❌ | `1` → فقط پزشکان **دارای نوبت** (تعریف پایین). `0` → فقط پزشکانی که فلگ `active_doctor_appointment` آنها خاموش است (کاربرد ادمین). بدون این پارامتر → فقط پزشکان **فعال** (`active_doctor_appointment` روشن)؛ پزشکان غیرفعال هرگز در لیست عمومی نمیآیند |
|
||
|
||
### مرتبسازی و تعریف «دارای نوبت»
|
||
|
||
پزشک **دارای نوبت** یعنی هر سه شرط برقرار باشد (همان تعریفی که فیلد `active` هر آیتم پاسخ را میسازد):
|
||
1. فلگ `active_doctor_appointment` روشن،
|
||
2. `WeeklySchedule` ثبتشده با حداقل یک سشن `active: true`،
|
||
3. `meta.online_booking_enabled` برابر `false` نباشد.
|
||
|
||
لیست همیشه اول پزشکان دارای نوبت را نشان میدهد و بعد بقیه را؛ داخل هر گروه بر اساس `doctorRate` و پارامتر `sort` مرتب میشود.
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"name": "علی احمدی",
|
||
"display_name": "دکتر علی احمدی",
|
||
"gender": "man",
|
||
"degree": "specialist",
|
||
"img": [],
|
||
"specialties": [{ "uuid": "...", "id": "1", "name": "قلب و عروق" }],
|
||
"satisfaction": "60",
|
||
"point": "3.5",
|
||
"free_turn": "دوشنبه 09:00–13:00",
|
||
"hours_of_work": "شنبه: 09:00–13:00 و 14:00–18:00 | یکشنبه: 09:00–13:00",
|
||
"active": true,
|
||
"owner_status": "claimed",
|
||
"city": [
|
||
{ "uuid": "7bfb989e-...", "id": "123", "name": "یاسوج", "parent": "23" }
|
||
],
|
||
"state": [
|
||
{ "uuid": "7bfb5705-...", "id": "23", "name": "کهگیلویه و بویراحمد" }
|
||
]
|
||
}
|
||
],
|
||
"meta": {
|
||
"totalRecords": 50,
|
||
"totalPages": 3,
|
||
"currentPage": 1,
|
||
"limit": 50
|
||
}
|
||
}
|
||
```
|
||
|
||
> ℹ️ `point` و `satisfaction` فقط برای `owner_status="claimed"` مقدار دارند؛ برای `unclaimed`/`pending_transfer` هر دو `null` هستند.
|
||
|
||
### اعتبارسنجی نام پزشک
|
||
|
||
`name` نمیتواند شمارهتلفن (`^0?9\d{9}$`) یا مقدار آزمایشی (`test`، `تست`، `-`، `null`) باشد. این مقادیر در **هر** مسیر نوشتن با `422` رد میشوند — API عمومی، پنل ادمین، import و دعوت کلینیک — چون گارد روی خودِ Entity نشسته است (`App\Shared\Util\DisplayName`).
|
||
|
||
دلیل: نام پزشک در `<title>` و نتایج جستوجوی سایت عمومی رندر میشود؛ رکوردی با نام «09390039833» یک صفحهٔ بیارزش ایندکسشدنی میسازد.
|
||
|
||
> دعوت پزشک توسط کلینیک، اگر نام ارسال نشود، دیگر شمارهٔ موبایل را بهعنوان نام نمینشاند — برچسب خنثای «پزشک دعوتشده» میگیرد تا خود پزشک پروفایلش را claim کند. (ریشهٔ آلودگی تولیدی همین بود.)
|
||
|
||
فرمان ممیزی رکوردهای موجود:
|
||
|
||
```bash
|
||
php bin/console app:audit-polluted-records # فقط گزارش
|
||
php bin/console app:audit-polluted-records --force # خارجکردن از انتشار (بدون حذف)
|
||
```
|
||
|
||
### `city` / `state` در پاسخ لیست
|
||
|
||
آرایه با حداکثر یک عضو — همشکل با `city`/`state` در پاسخ جزئیات پزشک و پاسخ لیست کلینیکها.
|
||
|
||
- منبع مکان **دقیقاً همان قاعدهای است که فیلتر `city_id`/`state_id` اعمال میکند**: اول آدرس شخصی پزشک (`doctor_addresses` با `doctor_id` مقداردار)، و اگر نداشت آدرس کلینیکی که عضو آن است (`doctor_addresses` با `clinic_id` مقداردار و `doctor_id` تهی). یعنی هر پزشکی که با `city_id=X` برگردد، در پاسخ هم همان شهر را اعلام میکند.
|
||
- پزشک چند-مطبی **یک شهر اصلی** میگیرد (اولین مکان یافتشده) — نه فهرست همهٔ شهرها.
|
||
- پزشک بدون هیچ آدرس: `"city": []` و `"state": []` (آرایهٔ خالی، نه `null`).
|
||
- `city[].parent` شناسهٔ استان است.
|
||
- استخراج مکان دستهای انجام میشود (`DoctorRepository::findLocationsByDoctors`) — حداکثر دو کوئری ثابت، مستقل از تعداد پزشکان در صفحه.
|
||
|
||
> 🔗 مصرفکننده: `nobat724_front/app/sitemap.js` با این فیلد تشخیص میدهد هر پزشک به کدام دامنهٔ شهری تعلق دارد (canonical). تغییر شکل این فیلد قرارداد آن را میشکند.
|
||
|
||
---
|
||
|
||
## PATCH `/api/v1/doctor/{uuid}`
|
||
|
||
Update doctor profile.
|
||
|
||
**Permission:** `AUTH` — must be the owner (or `ROLE_ADMIN`)
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `uuid` | string (UUID) | Doctor UUID |
|
||
|
||
### Request Body (`application/json`)
|
||
Same fields as POST (all optional), plus:
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `social_media` | object | Keys: `instagram`, `telegram`, `aparat`, `youtube`, `linkedin`. Each value must be a full valid URL or `null`. Any value that fails `FILTER_VALIDATE_URL` is silently stored as `null`. |
|
||
|
||
```json
|
||
{
|
||
"social_media": {
|
||
"instagram": "https://instagram.com/dr.example",
|
||
"telegram": "https://t.me/dr_example",
|
||
"aparat": null,
|
||
"youtube": null,
|
||
"linkedin": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### Response `200`
|
||
Updated doctor object (same structure as GET single).
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_AUTH_001` | 401 | Missing token |
|
||
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
|
||
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
|
||
|
||
---
|
||
|
||
## DELETE `/api/v1/doctor/{uuid}`
|
||
|
||
Delete a doctor profile.
|
||
|
||
**Permission:** `IS_AUTHENTICATED_FULLY` — **admin** (any profile) **or the owner** of a `claimed` profile (`doctor.user === current user`). Other users get `403`.
|
||
|
||
> **Side effect:** the doctor's insurance configuration (`tenant_insurances`, `entity_insurance_pricing`, and their `tenant_service_coverages`) is purged in the same request — these reference the doctor via a polymorphic `entity_id` with no DB FK, so the cleanup is enforced in the application.
|
||
|
||
> **Guard:** a doctor with existing appointments cannot be deleted (the `appointments.doctor_id` FK would otherwise raise a `500`). The endpoint pre-checks and returns `409 ERR_CONFLICT_001` instead.
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `uuid` | string (UUID) | Doctor UUID |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": { "message": "پزشک با موفقیت حذف شد" }
|
||
}
|
||
```
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_AUTH_001` | 401 | Missing token |
|
||
| `ERR_FORBIDDEN_001` | 403 | Not admin and not the owner of this claimed profile |
|
||
| `ERR_VALIDATION_002` | 404 | Doctor not found |
|
||
| `ERR_CONFLICT_001` | 409 | Doctor has existing appointments and cannot be deleted |
|
||
|
||
---
|
||
|
||
## POST `/file/upload/clinic_pro/doctor/field_image`
|
||
|
||
Upload doctor profile image.
|
||
|
||
**Permission:** `AUTH`
|
||
|
||
### Request
|
||
`Content-Type: multipart/form-data`
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `file` | binary | ✅ | Image file (max 5MB) |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"url": "https://clinic-pro.ddev.site/uploads/doctor/abc123.jpg",
|
||
"uuid": "...",
|
||
"filename": "abc123.jpg",
|
||
"filemime": "image/jpeg",
|
||
"filesize": 204800
|
||
}
|
||
}
|
||
```
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_FILE_001` | 422 | Invalid file type |
|
||
| `ERR_AUTH_001` | 401 | Missing token |
|
||
|
||
---
|
||
|
||
## GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`
|
||
|
||
Get all practice addresses for a doctor, including addresses of clinics the doctor is a member of.
|
||
|
||
**Permission:** `PUBLIC`
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `doctorId` | integer | Doctor's numeric ID (route requires `\d+`; non-numeric → 404) |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"id": "1",
|
||
"uuid": "...",
|
||
"type": "personal",
|
||
"clinic_id": null,
|
||
"clinic_name": null,
|
||
"name": "مطب تهران",
|
||
"address": "تهران، خیابان...",
|
||
"telephone": "02112345678",
|
||
"active": true,
|
||
"timezone": "Asia/Tehran",
|
||
"map": { "latitude": "35.6892", "longitude": "51.3890" },
|
||
"city": { "id": "1", "name": "تهران" },
|
||
"province": { "id": "1", "name": "تهران" }
|
||
},
|
||
{
|
||
"id": "5",
|
||
"uuid": "...",
|
||
"type": "clinic",
|
||
"clinic_id": 12,
|
||
"clinic_name": "کلینیک الوند",
|
||
"name": null,
|
||
"address": "اصفهان، خیابان...",
|
||
"telephone": "03112345678",
|
||
"active": true,
|
||
"timezone": "Asia/Tehran",
|
||
"map": { "latitude": null, "longitude": null },
|
||
"city": { "id": "3", "name": "اصفهان" },
|
||
"province": { "id": "2", "name": "اصفهان" }
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
> **نکته:** آدرسهای با `type: "clinic"` از کلینیکهایی که پزشک عضو آنهاست میآیند و `clinic_name` نام کلینیک را نشان میدهد.
|
||
|
||
> **`active` و `timezone` (افزودهشده در تسک شعبه):** هر آدرس یک «شعبه» است و این دو
|
||
> ویژگی روی خروجی **همهٔ** اندپوینتهای آدرس ظاهر میشوند، چون از
|
||
> `DoctorAddress::toArray()` میآیند. هر دو ستون `NOT NULL DEFAULT` دارند، پس ردیفهای
|
||
> قدیمی هم `active: true` و `timezone: "Asia/Tehran"` میدهند؛ تغییر additive است.
|
||
>
|
||
> `active` **فقط ذخیره میشود** و اثرش بر دسترسپذیری فقط همین است که منبعِ روی یک
|
||
> آدرسِ غیرفعال، روزش با دلیل `address_inactive` خالی برمیگردد. اندپوینت اختصاصیِ
|
||
> نوشتنِ این دو فیلد با حذف دامنهٔ شعبه برداشته شد.
|
||
|
||
---
|
||
|
||
## POST `/api/v1/clinic-pro/doctor-address`
|
||
|
||
Add a new practice address.
|
||
|
||
**Permission:** `AUTH` — must own the doctor profile
|
||
|
||
### Request Body
|
||
```json
|
||
{
|
||
"name": "مطب تهران",
|
||
"address": "تهران، خیابان ولیعصر",
|
||
"telephone": "02112345678",
|
||
"province_id": 1,
|
||
"city_id": 3,
|
||
"latitude": 35.6892,
|
||
"longitude": 51.3890
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required |
|
||
|-------|------|----------|
|
||
| `name` | string | ❌ |
|
||
| `address` | string | ✅ (frontend validation) |
|
||
| `telephone` | string | ✅ (frontend validation) |
|
||
| `province_id` | integer | ✅ (frontend validation) |
|
||
| `city_id` | integer | ✅ (frontend validation) |
|
||
| `latitude` | float | ❌ |
|
||
| `longitude` | float | ❌ |
|
||
|
||
### Response `201`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"id": 1,
|
||
"name": "مطب تهران",
|
||
"address": "تهران، خیابان ولیعصر",
|
||
"telephone": "02112345678",
|
||
"latitude": 35.6892,
|
||
"longitude": 51.3890
|
||
}
|
||
}
|
||
```
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_AUTH_001` | 401 | Missing token |
|
||
| `ERR_FORBIDDEN_001` | 403 | Not the doctor owner |
|
||
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
|
||
|
||
---
|
||
|
||
## PATCH `/api/v1/clinic-pro/doctor-address/{id}`
|
||
|
||
Update a practice address.
|
||
|
||
**Permission:** `AUTH` — must own the doctor profile
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `id` | integer | Address ID |
|
||
|
||
### Request Body
|
||
Same fields as POST — all optional.
|
||
|
||
### Response `200`
|
||
Updated address object.
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_AUTH_001` | 401 | Missing token |
|
||
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
|
||
| `ERR_NOT_FOUND_001` | 404 | Address not found |
|
||
|
||
---
|
||
|
||
## DELETE `/api/v1/clinic-pro/doctor-address/{id}`
|
||
|
||
Delete a practice address.
|
||
|
||
**Permission:** `AUTH` — must own the doctor profile
|
||
|
||
### Response `200`
|
||
```json
|
||
{ "success": true, "data": { "message": "آدرس حذف شد" } }
|
||
```
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_AUTH_001` | 401 | Missing token |
|
||
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
|
||
| `ERR_NOT_FOUND_001` | 404 | Address not found |
|
||
|
||
---
|
||
|
||
## POST `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}`
|
||
|
||
Create a doctor address automatically from a clinic's location.
|
||
|
||
**Permission:** `AUTH` — must own the doctor profile and be associated with the clinic
|
||
|
||
### Path Parameters
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `clinicUuid` | string (UUID) | Clinic UUID |
|
||
|
||
### Response `201`
|
||
Address object created from clinic data.
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_FORBIDDEN_001` | 403 | Not associated with this clinic |
|
||
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
|
||
|
||
---
|
||
|
||
## `GET /api/v1/addresses`
|
||
|
||
مجوز: `appointment_settings.view`. فهرست محلهای نوبتدهی محیط جاری، فقط برای انتخاب.
|
||
|
||
جانشین `GET /api/v1/branches` است که با حذف دامنهٔ شعبه برداشته شد. خودِ آدرس نمیرود:
|
||
هر منبع، لیست قیمت و نوبت به یکی از اینها بسته است. ساخت و ویرایش آدرس همانجایی است
|
||
که همیشه بود (`ClinicController` و `AppointmentSettingsController`).
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{ "uuid": "…", "name": "کلینیک تخصصی مهر", "active": true }
|
||
]
|
||
}
|
||
```
|
||
|
||
**۴۰۳:** منشیِ بدون مجوز `appointment_settings.view`.
|