From c662eb9b4f3d91905c3ce93681e888de3c28fc0a Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 15 Jun 2026 15:11:29 +0330 Subject: [PATCH] feat(doctor): update schedule fields in doctor API responses to reflect actual availability --- .claude/prompt/fix-doctor-schedule-fields.md | 250 ++++++++++++++++++ docs/api/doctor.md | 51 ++-- .../Repository/WeeklyScheduleRepository.php | 13 + src/Doctor/Controller/DoctorController.php | 21 +- src/Doctor/Entity/Doctor.php | 91 ++++++- 5 files changed, 398 insertions(+), 28 deletions(-) create mode 100644 .claude/prompt/fix-doctor-schedule-fields.md diff --git a/.claude/prompt/fix-doctor-schedule-fields.md b/.claude/prompt/fix-doctor-schedule-fields.md new file mode 100644 index 00000000..8b2407e9 --- /dev/null +++ b/.claude/prompt/fix-doctor-schedule-fields.md @@ -0,0 +1,250 @@ +# رفع فیلدهای free_turn و hours_of_work در API لیست پزشکان + +## زمینه + +endpoint `GET /api/v1/doctors` و `GET /api/v1/doctor/{uuid}` در پاسخ خود دو فیلد `free_turn` و `hours_of_work` دارند که همیشه مقدار ثابت «نوبت آزادی موجود نیست» و «برنامه کاری تنظیم نشده» برمی‌گردانند — حتی برای پزشکانی که برنامه هفتگی (`weekly_schedules`) ست کرده‌اند. همچنین `activeDoctorAppointment` (فیلد `active`) باید با وجود یا نبود برنامه هفتگی هماهنگ باشد. + +## مشکل / هدف + +- `free_turn`: باید نزدیک‌ترین روز کاری پزشک را نشان دهد (مثلاً «شنبه ۹:۰۰–۱۳:۰۰»)؛ اگر برنامه‌ای نداشت «نوبت آزادی موجود نیست» +- `hours_of_work`: باید ساعت‌های کاری روزهای فعال را خلاصه کند (مثلاً «شنبه تا چهارشنبه ۹–۱۳ و ۱۴–۱۸»)؛ اگر برنامه نداشت «برنامه کاری تنظیم نشده» +- `active` (`activeDoctorAppointment`): اگر پزشک برنامه هفتگی نداشته باشد یا همه روزها `sessions: []` باشند، باید `false` برگردد — نوبت‌دهی غیرفعال + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Doctor/Entity/Doctor.php` | متدهای `toListArray()` و `toDetailArray()` — مقادیر hardcoded اینجاست | +| `src/Doctor/Repository/DoctorRepository.php` | `findWithFilters()` — JOIN با weekly_schedules ندارد | +| `src/Doctor/Controller/DoctorController.php` | `list()` (خط ۲۳۳) و `show()` — از toListArray/toDetailArray استفاده می‌کنند | +| `src/Appointment/Entity/WeeklySchedule.php` | Entity برنامه هفتگی — فیلد `setting` آرایه ۷ عنصری (شنبه تا جمعه) | +| `src/Appointment/Repository/WeeklyScheduleRepository.php` | `findByDoctor(Doctor $doctor): ?WeeklySchedule` موجود است | + +## وضعیت فعلی + +### Doctor.php — toListArray() خط ۱۷۸: +```php +public function toListArray(): array +{ + return [ + // ... + 'free_turn' => 'نوبت آزادی موجود نیست', // ❌ hardcoded + 'hours_of_work' => 'برنامه کاری تنظیم نشده', // ❌ hardcoded + 'active' => $this->activeDoctorAppointment, + ]; +} +``` + +### Doctor.php — toDetailArray() خط ۱۹۸: +```php +'free_turn' => 'نوبت آزادی موجود نیست', // ❌ hardcoded +'hours_of_work' => 'برنامه کاری تنظیم نشده', // ❌ hardcoded +``` + +### ساختار setting در weekly_schedules: +آرایه ۷ عنصری (ایندکس ۰=شنبه، ۱=یکشنبه، ..., ۶=جمعه): +```json +[ + {"sessions": [{"active": true, "start_time": "09:00", "end_time": "13:00", ...}, ...]}, + {"sessions": [{"active": true, "start_time": "14:00", "end_time": "18:00", ...}]}, + ... + {"sessions": []} // جمعه — تعطیل +] +``` + +### DoctorRepository.findWithFilters() — JOIN ندارد: +```php +$qb = $this->createQueryBuilder('d') + ->leftJoin('d.specialties', 's') + ->leftJoin('d.provinces', 'pr') + ->leftJoin('d.cities', 'ci') + // ❌ هیچ JOIN با weekly_schedules ندارد +``` + +## وظایف + +### ۱. اضافه کردن متد کمکی به Doctor entity + +در `src/Doctor/Entity/Doctor.php` یک متد `computeScheduleFields(?WeeklySchedule $schedule): array` اضافه کن: + +```php +use App\Appointment\Entity\WeeklySchedule; + +private const DAY_NAMES = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']; + +public function computeScheduleFields(?WeeklySchedule $schedule): array +{ + if ($schedule === null) { + return [ + 'free_turn' => 'نوبت آزادی موجود نیست', + 'hours_of_work' => 'برنامه کاری تنظیم نشده', + 'has_schedule' => false, + ]; + } + + $setting = $schedule->getSetting(); // آرایه ۷ عنصری + + // محاسبه hours_of_work — ساعت‌های روزهای فعال + $workDays = []; + foreach ($setting as $dayIdx => $day) { + $activeSessions = array_filter( + $day['sessions'] ?? [], + fn($s) => ($s['active'] ?? false) && !empty($s['start_time']) + ); + if (!empty($activeSessions)) { + $times = array_map(fn($s) => $s['start_time'] . '–' . $s['end_time'], $activeSessions); + $workDays[] = self::DAY_NAMES[$dayIdx] . ': ' . implode(' و ', $times); + } + } + + if (empty($workDays)) { + return [ + 'free_turn' => 'نوبت آزادی موجود نیست', + 'hours_of_work' => 'برنامه کاری تنظیم نشده', + 'has_schedule' => false, + ]; + } + + // محاسبه free_turn — نزدیک‌ترین روز کاری فعال + // روز هفته فعلی را بگیر (PHP: 0=یکشنبه...6=شنبه → تبدیل به ایندکس ایرانی) + $phpDay = (int) date('w'); // 0=Sun, 6=Sat + $iranDay = $phpDay === 0 ? 1 : ($phpDay === 6 ? 0 : $phpDay + 1); // 0=Sat, 1=Sun, ... + + $freeTurn = null; + for ($i = 0; $i < 7; $i++) { + $idx = ($iranDay + $i) % 7; + $activeSessions = array_filter( + $setting[$idx]['sessions'] ?? [], + fn($s) => ($s['active'] ?? false) && !empty($s['start_time']) + ); + if (!empty($activeSessions)) { + $first = array_values($activeSessions)[0]; + $freeTurn = self::DAY_NAMES[$idx] . ' ' . $first['start_time'] . '–' . $first['end_time']; + break; + } + } + + return [ + 'free_turn' => $freeTurn ?? 'نوبت آزادی موجود نیست', + 'hours_of_work' => implode(' | ', $workDays), + 'has_schedule' => true, + ]; +} +``` + +### ۲. آپدیت toListArray() و toDetailArray() + +`toListArray()` و `toDetailArray()` باید یک `?WeeklySchedule` دریافت کنند: + +```php +public function toListArray(?WeeklySchedule $schedule = null): array +{ + $scheduleFields = $this->computeScheduleFields($schedule); + return [ + // ... بقیه فیلدها + 'free_turn' => $scheduleFields['free_turn'], + 'hours_of_work' => $scheduleFields['hours_of_work'], + 'active' => $this->activeDoctorAppointment && $scheduleFields['has_schedule'], + ]; +} + +public function toDetailArray(?WeeklySchedule $schedule = null): array +{ + $scheduleFields = $this->computeScheduleFields($schedule); + return [ + // ... بقیه فیلدها + 'free_turn' => $scheduleFields['free_turn'], + 'hours_of_work' => $scheduleFields['hours_of_work'], + 'active' => $this->activeDoctorAppointment && $scheduleFields['has_schedule'], + // ... address, state, city + ]; +} +``` + +### ۳. آپدیت DoctorRepository.findWithFilters() + +LEFT JOIN با `weekly_schedules` اضافه کن تا schedule را یکجا load کند: + +```php +$qb = $this->createQueryBuilder('d') + ->leftJoin('d.specialties', 's') + ->leftJoin('d.provinces', 'pr') + ->leftJoin('d.cities', 'ci') + ->addSelect('d') // ensure d is selected for eager loading + ->distinct(); +``` + +**نکته مهم:** از eager loading یا جداگانه query استفاده کن. چون Doctor entity OneToOne با WeeklySchedule ندارد (رابطه از طرف WeeklySchedule است)، بهترین راه این است که در Controller بعد از گرفتن لیست doctors، scheduleها را batch load کنی. + +### ۴. آپدیت DoctorController — متد list() + +در `src/Doctor/Controller/DoctorController.php` متد `list()` خط ۲۳۳: + +```php +public function __construct( + // اضافه کن: + private readonly WeeklyScheduleRepository $scheduleRepo, + // ... +) + +public function list(Request $request): JsonResponse +{ + $filters = $request->query->all(); + $result = $this->doctorRepo->findWithFilters($filters); + + // batch load schedules برای همه doctors + $scheduleMap = []; + foreach ($this->scheduleRepo->findByDoctors($result['items']) as $schedule) { + $scheduleMap[$schedule->getDoctor()->getId()] = $schedule; + } + + return $this->paginated( + array_map( + fn(Doctor $d) => $d->toListArray($scheduleMap[$d->getId()] ?? null), + $result['items'] + ), + $result['total'], + $result['page'], + $result['limit'] + ); +} +``` + +### ۵. اضافه کردن findByDoctors() به WeeklyScheduleRepository + +```php +/** + * @param Doctor[] $doctors + * @return WeeklySchedule[] + */ +public function findByDoctors(array $doctors): array +{ + if (empty($doctors)) return []; + + return $this->createQueryBuilder('ws') + ->where('ws.doctor IN (:doctors)') + ->setParameter('doctors', $doctors) + ->getQuery() + ->getResult(); +} +``` + +### ۶. آپدیت متدهای show() در DoctorController + +هر جایی که `$doctor->toDetailArray()` صدا زده می‌شود (خطوط ۱۱۷، ۱۶۳، ۱۸۴، ۳۱۲)، schedule را پاس بده: + +```php +$schedule = $this->scheduleRepo->findByDoctor($doctor); +return $this->success(['data' => $doctor->toDetailArray($schedule)]); +``` + +## نکات مهم + +- **Doctor entity رابطه مستقیم با WeeklySchedule ندارد** — رابطه OneToOne از طرف WeeklySchedule به Doctor است؛ پس `$doctor->getSchedule()` وجود ندارد و باید از Repository بخوانی +- **Circular dependency**: `Doctor.php` نباید مستقیم WeeklyScheduleRepository inject کند — بهتر است در Controller schedule را بگیری و به متد پاس بدهی (دقیقاً همان الگوی پیشنهادی بالا) +- **active flag**: فقط وقتی هم `activeDoctorAppointment=true` هم `has_schedule=true` باشد نوبت‌دهی فعال است +- **batch load**: برای endpoint لیست، حتماً از `findByDoctors()` batch استفاده کن — N+1 query نساز +- **ایندکس روزها**: `setting[0]=شنبه`, `setting[1]=یکشنبه`, ..., `setting[6]=جمعه` — PHP `date('w')` را باید به این ایندکس تبدیل کنی +- **sessions خالی**: اگر `sessions: []` باشد آن روز تعطیل است — فقط `active: true` و `start_time` غیرخالی معتبر است +- **مستندات**: بعد از تغییر، `docs/api/doctor.md` را آپدیت کن (فیلدهای `free_turn`، `hours_of_work`، `active` را توضیح بده) +- هیچ migration لازم نیست — Entity تغییر ساختاری ندارد diff --git a/docs/api/doctor.md b/docs/api/doctor.md index 0f0d98d7..772621ef 100644 --- a/docs/api/doctor.md +++ b/docs/api/doctor.md @@ -80,18 +80,23 @@ Get doctor detail with clinics. "data": { "data": { "uuid": "550e8400-...", - "title": "دکتر علی احمدی", - "gender": "male", + "name": "دکتر علی احمدی", + "gender": "man", "medical_system_code": "12345", - "degree": "متخصص", - "info": "...", - "image": "https://...", - "doctor_rate": 4.5, - "active_doctor_appointment": true, - "specialties": [{ "id": 1, "name": "قلب و عروق" }], - "doctor_services": [{ "id": 3, "name": "نوار قلب" }], - "clinics": [{ "uuid": "...", "name": "کلینیک الوند" }], - "created_at": 1717000000 + "degree": "specialist", + "detail": "...", + "img": [], + "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, + "specialties": [{ "uuid": "...", "id": "1", "name": "قلب و عروق", "parent_id": null }], + "expertise": [{ "uuid": "...", "id": "3", "name": "نوار قلب" }], + "address": [], + "state": [], + "city": [], + "clinics": [{ "uuid": "...", "name": "کلینیک الوند", "address": "...", "telephone": "..." }] } } } @@ -144,6 +149,16 @@ Get doctor detail for clinic owner — only doctors who are members of the authe --- +### Schedule Fields Notes + +| Field | When schedule exists | When no schedule | +|-------|---------------------|-----------------| +| `free_turn` | نزدیک‌ترین روز/ساعت کاری از امروز (مثلاً «دوشنبه ۹:۰۰–۱۳:۰۰») | «نوبت آزادی موجود نیست» | +| `hours_of_work` | خلاصه ساعت‌های روزهای فعال با `\|` جداشده | «برنامه کاری تنظیم نشده» | +| `active` | `activeDoctorAppointment && has_active_sessions` | `false` — نوبت‌دهی غیرفعال | + +--- + ## GET `/api/v1/doctors` List doctors with pagination and filters. @@ -167,10 +182,16 @@ List doctors with pagination and filters. "data": [ { "uuid": "...", - "title": "دکتر علی احمدی", - "degree": "متخصص", - "doctor_rate": 4.5, - "image": "https://..." + "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 } ], "meta": { diff --git a/src/Appointment/Repository/WeeklyScheduleRepository.php b/src/Appointment/Repository/WeeklyScheduleRepository.php index 87023080..431a8e16 100644 --- a/src/Appointment/Repository/WeeklyScheduleRepository.php +++ b/src/Appointment/Repository/WeeklyScheduleRepository.php @@ -19,6 +19,19 @@ class WeeklyScheduleRepository extends ServiceEntityRepository return $this->findOneBy(['doctor' => $doctor]); } + /** @param Doctor[] $doctors @return WeeklySchedule[] */ + public function findByDoctors(array $doctors): array + { + if (empty($doctors)) { + return []; + } + return $this->createQueryBuilder('ws') + ->where('ws.doctor IN (:doctors)') + ->setParameter('doctors', $doctors) + ->getQuery() + ->getResult(); + } + public function findByUuid(string $uuid): ?WeeklySchedule { return $this->findOneBy(['uuid' => $uuid]); diff --git a/src/Doctor/Controller/DoctorController.php b/src/Doctor/Controller/DoctorController.php index c66ee989..f5cd4222 100644 --- a/src/Doctor/Controller/DoctorController.php +++ b/src/Doctor/Controller/DoctorController.php @@ -2,6 +2,7 @@ namespace App\Doctor\Controller; +use App\Appointment\Repository\WeeklyScheduleRepository; use App\Auth\Entity\User; use App\Auth\Repository\UserRepository; use App\Clinic\Entity\Clinic; @@ -37,6 +38,7 @@ class DoctorController extends BaseController private readonly CityRepository $cityRepo, private readonly UserRepository $userRepo, private readonly FileValidatorService $fileValidator, + private readonly WeeklyScheduleRepository $scheduleRepo, private readonly string $projectDir, ) {} @@ -114,7 +116,8 @@ class DoctorController extends BaseController $this->userRepo->save($user); } - return $this->success(['data' => $doctor->toDetailArray()], 201); + $schedule = $this->scheduleRepo->findByDoctor($doctor); + return $this->success(['data' => $doctor->toDetailArray($schedule)], 201); } #[OA\Get( @@ -160,7 +163,8 @@ class DoctorController extends BaseController ], ], $clinics); - return $this->success(['data' => array_merge($doctor->toDetailArray(), ['clinics' => $clinicData])]); + $schedule = $this->scheduleRepo->findByDoctor($doctor); + return $this->success(['data' => array_merge($doctor->toDetailArray($schedule), ['clinics' => $clinicData])]); } #[Route('/api/v1/clinic/my-doctor/{doctorUuid}', methods: ['GET'])] @@ -181,7 +185,8 @@ class DoctorController extends BaseController return $this->error(ErrorCodes::ERR_AUTH_006, 'این پزشک عضو کلینیک شما نیست', 403); } - return $this->success(['data' => array_merge($doctor->toDetailArray(), ['clinics' => [[ + $schedule = $this->scheduleRepo->findByDoctor($doctor); + return $this->success(['data' => array_merge($doctor->toDetailArray($schedule), ['clinics' => [[ 'id' => (string) $clinic->getId(), 'uuid' => $clinic->getUuid(), 'name' => $clinic->getName(), @@ -235,8 +240,13 @@ class DoctorController extends BaseController $filters = $request->query->all(); $result = $this->doctorRepo->findWithFilters($filters); + $scheduleMap = []; + foreach ($this->scheduleRepo->findByDoctors($result['items']) as $schedule) { + $scheduleMap[$schedule->getDoctor()->getId()] = $schedule; + } + return $this->paginated( - array_map(fn(Doctor $d) => $d->toListArray(), $result['items']), + array_map(fn(Doctor $d) => $d->toListArray($scheduleMap[$d->getId()] ?? null), $result['items']), $result['total'], $result['page'], $result['limit'] @@ -309,7 +319,8 @@ class DoctorController extends BaseController $this->hydrateDoctor($doctor, $data); $this->doctorRepo->save($doctor); - return $this->success(['data' => $doctor->toDetailArray()]); + $schedule = $this->scheduleRepo->findByDoctor($doctor); + return $this->success(['data' => $doctor->toDetailArray($schedule)]); } #[OA\Delete( diff --git a/src/Doctor/Entity/Doctor.php b/src/Doctor/Entity/Doctor.php index c7dc1e64..152f54d7 100644 --- a/src/Doctor/Entity/Doctor.php +++ b/src/Doctor/Entity/Doctor.php @@ -2,6 +2,7 @@ namespace App\Doctor\Entity; +use App\Appointment\Entity\WeeklySchedule; use App\Auth\Entity\User; use App\DoctorService\Entity\DoctorService; use App\Location\Entity\City; @@ -167,6 +168,78 @@ class Doctor private function touch(): void { $this->updatedAt = time(); } + private const DAY_NAMES = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']; + + private function computeScheduleFields(?WeeklySchedule $schedule): array + { + if ($schedule === null) { + return ['free_turn' => 'نوبت آزادی موجود نیست', 'hours_of_work' => 'برنامه کاری تنظیم نشده', 'has_schedule' => false]; + } + + $setting = $schedule->getSetting(); + + // استخراج ساعت‌های هر روز — key: dayIdx، value: رشته ساعت‌ها یا null + $dayTimes = []; + for ($i = 0; $i < 7; $i++) { + $activeSessions = array_values(array_filter( + $setting[$i]['sessions'] ?? [], + fn($s) => ($s['active'] ?? false) && !empty($s['start_time']) + )); + $dayTimes[$i] = empty($activeSessions) + ? null + : implode(' و ', array_map(fn($s) => $s['start_time'] . '–' . $s['end_time'], $activeSessions)); + } + + $hasAnyDay = array_filter($dayTimes) !== []; + if (!$hasAnyDay) { + return ['free_turn' => 'نوبت آزادی موجود نیست', 'hours_of_work' => 'برنامه کاری تنظیم نشده', 'has_schedule' => false]; + } + + // گروه‌بندی روزهای متوالی با ساعت یکسان + // مثال: شنبه–پنجشنبه ۹–۱۳ و ۱۴–۱۸ | جمعه تعطیل + $groups = []; + $current = ['start' => 0, 'times' => $dayTimes[0]]; + for ($i = 1; $i < 7; $i++) { + if ($dayTimes[$i] === $current['times']) { + continue; // ادامه همان گروه + } + if ($current['times'] !== null) { + $groups[] = ['start' => $current['start'], 'end' => $i - 1, 'times' => $current['times']]; + } + $current = ['start' => $i, 'times' => $dayTimes[$i]]; + } + if ($current['times'] !== null) { + $groups[] = ['start' => $current['start'], 'end' => 6, 'times' => $current['times']]; + } + + $parts = []; + foreach ($groups as $g) { + $parts[] = $g['start'] === $g['end'] + ? self::DAY_NAMES[$g['start']] + : self::DAY_NAMES[$g['start']] . ' تا ' . self::DAY_NAMES[$g['end']]; + } + + // نزدیک‌ترین روز کاری — PHP date('w'): 0=Sun,6=Sat → ایندکس ایرانی: 0=Sat,...,6=Fri + $phpDay = (int) date('w'); + $iranDay = $phpDay === 0 ? 1 : ($phpDay === 6 ? 0 : $phpDay + 1); + + $freeTurn = null; + for ($i = 0; $i < 7; $i++) { + $idx = ($iranDay + $i) % 7; + if ($dayTimes[$idx] !== null) { + $firstTime = explode(' و ', $dayTimes[$idx])[0]; + $freeTurn = self::DAY_NAMES[$idx] . ' ' . $firstTime; + break; + } + } + + return [ + 'free_turn' => $freeTurn ?? 'نوبت آزادی موجود نیست', + 'hours_of_work' => implode(' | ', $parts), + 'has_schedule' => true, + ]; + } + public function getExperience(): int { if ($this->activityTime === null) { @@ -175,8 +248,9 @@ class Doctor return max(0, (int)((time() - $this->activityTime) / (365.25 * 24 * 3600))); } - public function toListArray(): array + public function toListArray(?WeeklySchedule $schedule = null): array { + $sf = $this->computeScheduleFields($schedule); return [ 'id' => (string) $this->id, 'uuid' => $this->uuid, @@ -189,14 +263,15 @@ class Doctor ], $this->specialties->toArray()), 'satisfaction' => (string) $this->doctorRatePercentage, 'point' => (string) $this->doctorRate, - 'free_turn' => 'نوبت آزادی موجود نیست', - 'hours_of_work' => 'برنامه کاری تنظیم نشده', - 'active' => $this->activeDoctorAppointment, + 'free_turn' => $sf['free_turn'], + 'hours_of_work' => $sf['hours_of_work'], + 'active' => $this->activeDoctorAppointment && $sf['has_schedule'], ]; } - public function toDetailArray(): array + public function toDetailArray(?WeeklySchedule $schedule = null): array { + $sf = $this->computeScheduleFields($schedule); return [ 'id' => (string) $this->id, 'uuid' => $this->uuid, @@ -211,15 +286,15 @@ class Doctor 'uuid' => $s->getUuid(), 'id' => (string) $s->getId(), 'name' => $s->getName(), 'parent_id' => $s->getParent()?->getId() !== null ? (string) $s->getParent()->getId() : null, ], $this->specialties->toArray()), - 'active' => $this->activeDoctorAppointment, + 'active' => $this->activeDoctorAppointment && $sf['has_schedule'], 'img' => $this->images ?? [], 'expertise' => array_map(fn(DoctorService $ds) => [ 'uuid' => $ds->getUuid(), 'id' => (string) $ds->getId(), 'name' => $ds->getName(), ], $this->services->toArray()), 'satisfaction' => (string) $this->doctorRatePercentage, 'point' => (string) $this->doctorRate, - 'free_turn' => 'نوبت آزادی موجود نیست', - 'hours_of_work' => 'برنامه کاری تنظیم نشده', + 'free_turn' => $sf['free_turn'], + 'hours_of_work' => $sf['hours_of_work'], 'address' => array_map(fn(DoctorAddress $a) => $a->toArray(), $this->addresses->toArray()), 'average_rate' => ['total_rates' => null], 'state' => array_map(fn(Province $p) => [