feat(treatment): expose treatment cases, the unbooked queue and slot suggestions
Booking the next session stays a decision, not an automation: the system offers free slots and the secretary picks one with the patient in front of them. Booking automatically would fill the worst slot in the calendar — the one nobody wanted — and produce a no-show. A session whose due date has passed with nobody booking it surfaces in an explicit queue instead of waiting silently for the patient to call. Suggestions default to the resource the previous session ran on, since continuing a course on the same device is both clinically steadier and one less choice to make; with no previous booking the caller must name a resource rather than get an empty list. Slot maths is reused from ResourceBookingSlotService; this only decides which resource, from which day, and how far ahead to look. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Treatment\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Doctor\Service\AddressResolver;
|
||||
use App\Resource\Repository\ClinicResourceRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Exception\AppException;
|
||||
use App\Shared\Tenant\TenantOwnershipChecker;
|
||||
use App\Treatment\Entity\TreatmentCase;
|
||||
use App\Treatment\Entity\TreatmentSession;
|
||||
use App\Treatment\Repository\TreatmentCaseRepository;
|
||||
use App\Treatment\Repository\TreatmentSessionRepository;
|
||||
use App\Treatment\Service\NextSessionSlotFinder;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[OA\Tag(name: 'Treatment')]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
class TreatmentCaseController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TreatmentCaseRepository $cases,
|
||||
private readonly TreatmentSessionRepository $sessions,
|
||||
private readonly ClinicResourceRepository $resources,
|
||||
private readonly NextSessionSlotFinder $slotFinder,
|
||||
private readonly TenantOwnershipChecker $ownership,
|
||||
private readonly AddressResolver $branches,
|
||||
) {}
|
||||
|
||||
#[Route('/api/v1/treatment-cases', name: 'treatment_case_list', methods: ['GET'])]
|
||||
public function list(#[CurrentUser] User $user, Request $request): JsonResponse
|
||||
{
|
||||
[$entityType, $entityId] = $this->branches->pair($user);
|
||||
|
||||
$status = $request->query->get('status');
|
||||
$status = is_string($status) && $status !== '' ? $status : null;
|
||||
|
||||
return $this->success(array_map(
|
||||
static fn (TreatmentCase $c): array => $c->toArray(),
|
||||
$this->cases->findForTenant($entityType, $entityId, $status),
|
||||
));
|
||||
}
|
||||
|
||||
#[Route('/api/v1/treatment-case/{uuid}', name: 'treatment_case_show', methods: ['GET'])]
|
||||
public function show(#[CurrentUser] User $user, string $uuid): JsonResponse
|
||||
{
|
||||
return $this->success($this->requireCase($user, $uuid)->toArray(withSessions: true));
|
||||
}
|
||||
|
||||
/**
|
||||
* صفِ «جلسات بدون نوبت» — کاری که منشی باید انجام دهد، نه رفتاری که خودکار اتفاق بیفتد.
|
||||
*
|
||||
* جلسهای که سررسیدش رسیده و کسی رزروش نکرده اینجا دیده میشود؛ بدون این، جلسهٔ
|
||||
* فراموششده در سکوت میماند تا بیمار خودش زنگ بزند.
|
||||
*/
|
||||
#[Route('/api/v1/treatment-sessions/unbooked', name: 'treatment_sessions_unbooked', methods: ['GET'])]
|
||||
public function unbooked(#[CurrentUser] User $user, Request $request): JsonResponse
|
||||
{
|
||||
[$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),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* اسلاتهای پیشنهادی برای این جلسه.
|
||||
*
|
||||
* پیشنهاد است، نه رزرو: منشی با بیمار هماهنگ میکند و بعد از مسیر عادی ثبت نوبت
|
||||
* یکی را میگیرد.
|
||||
*/
|
||||
#[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
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user