Two holes in how a course's later appointments were made.
The link from the unbooked queue carried nothing — `/admin/appointments/new`
with no parameters — so the secretary retyped the patient and the service, and
which case the appointment joined was inferred from the service they happened to
pick. A patient with two open courses had no way to say which one they meant,
and picking the wrong service silently opened a third case. (The suggestion link
did pass slot_start and resource_uuid, but the create page never read either.)
POST /api/v1/my/appointment now takes an optional treatment_session_uuid.
SessionBookingLink validates it — same tenant, still unbooked, case open, same
patient — and reserves that session. Confirm-time attachment steps aside when
the appointment already holds a session. The booking form states in words which
session, which course and which patient it is about to book, read from a new
GET /api/v1/treatment-session/{uuid}.
Nothing ever detached a session from its appointment, so a cancelled booking
left the session `booked` forever, and since findNextUnbooked requires
"has no appointment", it could never return to the queue. Cancellation and
no-show now release it back to `planned`. A finished session is history and is
left alone.
The system still never books the next appointment by itself — it only suggests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.9 KiB
PHP
107 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Treatment\Workflow;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\ClinicService\Entity\ServiceItem;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Treatment\Entity\TreatmentCase;
|
|
use App\Treatment\Entity\TreatmentProtocol;
|
|
use App\Treatment\Entity\TreatmentSession;
|
|
use App\Treatment\Repository\TreatmentCaseRepository;
|
|
use App\Treatment\Repository\TreatmentSessionRepository;
|
|
use App\Treatment\Service\TreatmentCaseOpener;
|
|
use App\Treatment\Service\TreatmentScheduler;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
/**
|
|
* رفتار دورهٔ درمان وقتی حوزهٔ فعالیت پیادهسازی اختصاصی ندارد.
|
|
*
|
|
* عمداً پایینترین اولویت را دارد و به هر کدی جواب مثبت میدهد، پس محیطی که هنوز
|
|
* حوزهاش را انتخاب نکرده هم دورهٔ چندجلسهایاش کار میکند — تصمیمِ «نال یعنی رفتار
|
|
* امروز، نه خطا».
|
|
*/
|
|
class DefaultTreatmentWorkflow implements TreatmentWorkflow
|
|
{
|
|
public function __construct(
|
|
protected readonly TreatmentCaseRepository $cases,
|
|
protected readonly TreatmentSessionRepository $sessions,
|
|
protected readonly TreatmentCaseOpener $opener,
|
|
protected readonly TreatmentScheduler $scheduler,
|
|
protected readonly EntityManagerInterface $em,
|
|
) {}
|
|
|
|
public function supports(?string $practiceDomainCode): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function openCase(
|
|
Appointment $appointment,
|
|
PatientRecord $record,
|
|
ServiceItem $service,
|
|
TreatmentProtocol $protocol,
|
|
): TreatmentCase {
|
|
// بیماری که وسط دورهاش نوبت دیگری از همان سرویس میگیرد، جلسهٔ همان دوره را
|
|
// میگیرد نه یک دورهٔ موازی.
|
|
$case = $this->cases->findOpenFor($record, $service)
|
|
?? $this->opener->open(
|
|
$record->getEntityType(),
|
|
$record->getEntityId(),
|
|
$record,
|
|
$service,
|
|
$protocol,
|
|
);
|
|
|
|
$this->attachToNextOpenSession($case, $appointment);
|
|
|
|
return $case;
|
|
}
|
|
|
|
public function onSessionFinished(TreatmentSession $session): void
|
|
{
|
|
$next = $this->scheduler->scheduleNextAfter($session);
|
|
|
|
if ($next === null) {
|
|
$this->closeIfEverySessionIsSettled($session->getTreatmentCase());
|
|
}
|
|
|
|
$this->em->flush();
|
|
}
|
|
|
|
/**
|
|
* نوبت به اولین جلسهٔ بدونِ نوبت وصل میشود، نه لزوماً به جلسهٔ اول.
|
|
*
|
|
* مگر اینکه منشی هنگام ثبت، جلسه را صریح انتخاب کرده باشد؛ آنوقت نوبت از قبل
|
|
* وصل است و حدسِ «اولین بدون نوبت» فقط جلسهٔ دیگری را اشتباهی رزرو میکند.
|
|
*/
|
|
protected function attachToNextOpenSession(TreatmentCase $case, Appointment $appointment): void
|
|
{
|
|
if ($this->sessions->findOneBy(['appointment' => $appointment]) !== null) {
|
|
return;
|
|
}
|
|
|
|
$session = $this->sessions->findNextUnbooked($case);
|
|
|
|
if ($session === null) {
|
|
return;
|
|
}
|
|
|
|
$session->attachAppointment($appointment);
|
|
// سررسیدِ جلسهای که نوبت گرفته دیگر حدس نیست — همان ساعت نوبت است.
|
|
$this->scheduler->seedFirstSession($session, $appointment->getSlotStart());
|
|
$this->em->flush();
|
|
}
|
|
|
|
protected function closeIfEverySessionIsSettled(TreatmentCase $case): void
|
|
{
|
|
foreach ($case->getSessions() as $session) {
|
|
if (!in_array($session->getStatus(), [TreatmentSession::STATUS_DONE, TreatmentSession::STATUS_CANCELLED], true)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$case->close(TreatmentCase::STATUS_COMPLETED);
|
|
}
|
|
}
|