secretaryAccess->denyUnlessGranted($user, 'treatment', $action); $this->clinicDoctorAccess->denyUnlessGranted($user, 'treatment', $action); } #[Route('/api/v1/treatment-cases', name: 'treatment_case_list', methods: ['GET'])] public function list(#[CurrentUser] User $user, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'view'); [$entityType, $entityId] = $this->branches->pair($user); $status = $request->query->get('status'); $status = is_string($status) && $status !== '' ? $status : null; $q = $request->query->get('q'); $q = is_string($q) ? trim($q) : ''; return $this->success(array_map( static fn (TreatmentCase $c): array => $c->toArray(), $this->cases->findForTenant( $entityType, $entityId, $status, $q !== '' ? $q : null, $this->dayBoundary($request->query->get('from'), '00:00:00'), $this->dayBoundary($request->query->get('to'), '23:59:59'), // نمای «پروندهٔ بیمار» همین فهرست است با یک کران بیشتر. is_string($record = $request->query->get('record')) && $record !== '' ? $record : null, ), )); } /** * `YYYY-MM-DD` میلادی → ثانیهٔ ابتدای/انتهای همان روز. * * تایم‌زون سراسری اپلیکیشن تهران است (`config/bootstrap_tz.php`)، پس همان * `strtotime` که بقیهٔ فیلترهای تاریخِ نوبت‌ها استفاده می‌کنند اینجا هم درست است. */ private function dayBoundary(mixed $value, string $time): ?int { if (!is_string($value) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { return null; } $ts = strtotime($value . ' ' . $time); return $ts === false ? null : $ts; } #[Route('/api/v1/treatment-case/{uuid}', name: 'treatment_case_show', methods: ['GET'])] public function show(#[CurrentUser] User $user, string $uuid): JsonResponse { $this->denyUnlessGranted($user, 'view'); $case = $this->requireCase($user, $uuid); /** * نواحیِ قابل انتخاب کنار خودِ پرونده می‌آید، وگرنه فرم ویرایش باید حدس بزند * کدام دسته‌ها مجازند یا اندپوینت دومی برای همان یک سؤال ساخته شود. */ return $this->success($case->toArray(withSessions: true) + [ 'available_areas' => array_map( static fn (CatalogCategory $c): array => ['uuid' => $c->getUuid(), 'name' => $c->getName()], $this->opener->resolveAreas($case->getServiceItem()), ), ]); } /** * ویرایش پروندهٔ باز — وضعیت، پزشک ناظر، نواحی و تعداد جلسات. * * قواعدش در {@see TreatmentCaseEditor} است نه اینجا: هیچ ویرایشی نباید سابقهٔ * انجام‌شده را بازنویسی کند و آن تصمیم جای کنترلر نیست. */ #[Route('/api/v1/treatment-case/{uuid}', name: 'treatment_case_update', methods: ['PATCH'])] public function update(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'update'); $data = json_decode($request->getContent(), true); if (!is_array($data)) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422); } $case = $this->editor->update($this->requireCase($user, $uuid), $data); return $this->success($case->toArray(withSessions: true)); } /** * صفِ «جلسات بدون نوبت» — کاری که منشی باید انجام دهد، نه رفتاری که خودکار اتفاق بیفتد. * * جلسه‌ای که سررسیدش رسیده و کسی رزروش نکرده اینجا دیده می‌شود؛ بدون این، جلسهٔ * فراموش‌شده در سکوت می‌ماند تا بیمار خودش زنگ بزند. */ #[Route('/api/v1/treatment-sessions/unbooked', name: 'treatment_sessions_unbooked', methods: ['GET'])] public function unbooked(#[CurrentUser] User $user, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'view'); [$entityType, $entityId] = $this->branches->pair($user); $withinDays = (int) $request->query->get('within_days', 7); $until = time() + max(0, min($withinDays, 90)) * 86400; return $this->success(array_map( static fn (TreatmentSession $s): array => $s->toArray() + [ 'case_uuid' => $s->getTreatmentCase()->getUuid(), 'service_name' => $s->getTreatmentCase()->getServiceItem()->getName(), ], $this->sessions->findUnbookedDue($entityType, $entityId, $until), )); } /** * تقویمِ کل دوره + جزئیات آنچه انجام شده. * * جدا از `GET /treatment-case/{uuid}` است نه اضافه به آن: آن پاسخ مصرف‌کنندهٔ * دیگری دارد (مودال ویرایش) که نه تقویم لازم دارد نه نواحی، و بزرگ‌ترش کردن یعنی * هزینهٔ بی‌مصرف روی همان مسیر. * * `planned_at` تخمین است نه دادهٔ ذخیره‌شده — `is_estimate` تکلیفش را روشن می‌کند. */ #[Route('/api/v1/treatment-case/{uuid}/plan', name: 'treatment_case_plan', methods: ['GET'])] public function plan(#[CurrentUser] User $user, string $uuid): JsonResponse { $this->denyUnlessGranted($user, 'view'); $case = $this->requireCase($user, $uuid); /** * دستگاهِ دوره — همان که جلسهٔ قبل رویش انجام شد. * * فرم ثبت نوبت بدون آن کار نمی‌کند: سرویسِ دوره روی تقویم منبع رزرو می‌شود نه * روی برنامهٔ پزشک، پس مودال باید در حالت منبع باز شود. */ $resource = $this->slotFinder->preferredResource($case); /** * کد ملی برای پیش‌پرکردنِ فرم ثبت نوبت لازم است و روی پروفایل می‌نشیند، نه * روی کاربر — همان COALESCE که `PatientController` هم می‌کند. بدونش منشی * بیماری را دوباره جستجو می‌کند که همین‌جا معلوم است کیست. */ $patientUser = $case->getPatientRecord()->getUser(); $nationalCode = $this->profiles->findOneBy(['user' => $patientUser])?->getNationalCode() ?? $patientUser->getNationalCode(); return $this->success([ 'case' => $case->toArray() + [ 'patient_national_code' => $nationalCode, ], 'resource' => $resource === null ? null : [ 'uuid' => $resource->getUuid(), 'name' => $resource->getName(), ], 'sessions' => array_map( static fn (array $row): array => $row['session']->toArray(withAreas: true) + [ 'planned_at' => $row['planned_at'], 'is_estimate' => $row['is_estimate'], ], $this->planner->project($case), ), ]); } /** * یک جلسه به‌تنهایی — برای فرمِ «ثبت نوبت این جلسه». * * فرم باید بگوید نوبت برای کدام دوره و کدام بیمار ثبت می‌شود؛ بیمارِ چنددوره‌ای * بدون این، اتصال را به حدسِ سرویس می‌سپارد. */ #[Route('/api/v1/treatment-session/{uuid}', name: 'treatment_session_show', methods: ['GET'])] public function showSession(#[CurrentUser] User $user, string $uuid): JsonResponse { $this->denyUnlessGranted($user, 'view'); $session = $this->requireSession($user, $uuid); $case = $session->getTreatmentCase(); return $this->success($session->toArray() + [ 'case_uuid' => $case->getUuid(), 'service' => ['uuid' => $case->getServiceItem()->getUuid(), 'name' => $case->getServiceItem()->getName()], 'patient' => [ 'record_uuid' => $case->getPatientRecord()->getUuid(), 'name' => $case->getPatientRecord()->getUser()->getRealName(), ], ]); } /** * اسلات‌های پیشنهادی برای این جلسه. * * پیشنهاد است، نه رزرو: منشی با بیمار هماهنگ می‌کند و بعد از مسیر عادی ثبت نوبت * یکی را می‌گیرد. */ #[Route('/api/v1/treatment-session/{uuid}/slot-suggestions', name: 'treatment_session_slots', methods: ['GET'])] public function slotSuggestions(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse { $this->denyUnlessGranted($user, 'view'); $session = $this->requireSession($user, $uuid); $resourceUuid = $request->query->get('resource_uuid'); $resource = is_string($resourceUuid) && $resourceUuid !== '' ? $this->resources->findByUuid($resourceUuid) : $this->slotFinder->preferredResource($session->getTreatmentCase()); if ($resource === null) { return $this->error( ErrorCodes::ERR_VALIDATION_002, 'منبعی برای پیشنهاد وقت مشخص نیست؛ resource_uuid بفرستید', 422, 'resource_uuid', ); } [$entityType, $entityId] = $this->branches->pair($user); if (!$this->ownership->belongsToPair($entityType, $entityId, $resource)) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'منبع یافت نشد', 404, 'resource_uuid'); } $days = (int) $request->query->get('days', NextSessionSlotFinder::DEFAULT_HORIZON_DAYS); return $this->success($this->slotFinder->suggest($session, $resource, $days) + [ 'session' => $session->toArray(), ]); } private function requireCase(User $user, string $uuid): TreatmentCase { [$entityType, $entityId] = $this->branches->pair($user); $case = $this->cases->findByUuid($uuid); if (!$this->ownership->belongsToPair($entityType, $entityId, $case)) { throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'پروندهٔ درمان یافت نشد', 404); } return $case; } private function requireSession(User $user, string $uuid): TreatmentSession { [$entityType, $entityId] = $this->branches->pair($user); $session = $this->sessions->findByUuid($uuid); if (!$this->ownership->belongsToPair($entityType, $entityId, $session)) { throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'جلسهٔ درمان یافت نشد', 404); } return $session; } }