The list could be narrowed by status and by search but not by when a case opened, which is the one axis a clinic actually reports on. `from` and `to` (YYYY-MM-DD) now bound `opened_at`, using the same strtotime day-boundary convention the appointment date filters already use under the app's global Tehran timezone. A malformed value is ignored rather than erroring — this is a filter, not a form field. Both bounds live in the URL via useUrlState, so back and refresh keep the range. The two date inputs and the "تا" between them are one nowrap unit; letting them wrap separately orphaned the word from its field on a 390px screen. The card's "شروع" showed only the Jalali date, so several cases opened on the same day were indistinguishable on that line too. It now uses formatDateTime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
245 lines
9.5 KiB
PHP
245 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Treatment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\ClinicService\Entity\CatalogCategory;
|
|
use App\ClinicService\Entity\ServiceItem;
|
|
use App\ClinicService\Entity\ServiceSection;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Tests\ApiTestCase;
|
|
use App\Treatment\Entity\TreatmentCase;
|
|
use App\Treatment\Entity\TreatmentCaseArea;
|
|
use App\Treatment\Entity\TreatmentProtocol;
|
|
use App\Treatment\Entity\TreatmentProtocolStep;
|
|
use App\Treatment\Entity\TreatmentSession;
|
|
use App\Treatment\Repository\TreatmentCaseRepository;
|
|
use App\Treatment\Service\TreatmentCaseEditor;
|
|
|
|
/**
|
|
* ویرایش پروندهٔ باز، با یک مرز ثابت: سابقهٔ انجامشده بازنویسی نمیشود.
|
|
*/
|
|
class TreatmentCaseEditTest extends ApiTestCase
|
|
{
|
|
private function editor(): TreatmentCaseEditor
|
|
{
|
|
return static::getContainer()->get(TreatmentCaseEditor::class);
|
|
}
|
|
|
|
private function cases(): TreatmentCaseRepository
|
|
{
|
|
return static::getContainer()->get(TreatmentCaseRepository::class);
|
|
}
|
|
|
|
/**
|
|
* @return array{TreatmentCase, Clinic, ServiceItem, array<string, CatalogCategory>}
|
|
*/
|
|
private function scenario(string $patientName = 'سارا کاظمی'): array
|
|
{
|
|
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر ویرایش');
|
|
$this->em->persist($doctor);
|
|
|
|
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
|
|
$clinic->setName('کلینیک ویرایش ' . uniqid());
|
|
$clinic->getDoctors()->add($doctor);
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
$section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر');
|
|
$this->em->persist($section);
|
|
|
|
$categories = [];
|
|
foreach (['دست', 'پا', 'صورت'] as $name) {
|
|
$c = new CatalogCategory('clinic', (int) $clinic->getId(), $name);
|
|
$this->em->persist($c);
|
|
$categories[$name] = $c;
|
|
}
|
|
|
|
$service = new ServiceItem($section, 'لیزر بدن', 4_000_000);
|
|
$this->em->persist($service);
|
|
$this->em->flush();
|
|
|
|
$protocol = new TreatmentProtocol($service);
|
|
$this->em->persist($protocol);
|
|
$protocol->replaceSteps([
|
|
new TreatmentProtocolStep($protocol, 1, 0),
|
|
new TreatmentProtocolStep($protocol, 2, 15),
|
|
new TreatmentProtocolStep($protocol, 3, 30),
|
|
]);
|
|
$this->em->flush();
|
|
|
|
$patient = $this->createUser(['ROLE_USER']);
|
|
$patient->setRealName($patientName);
|
|
$record = new PatientRecord('clinic', (int) $clinic->getId(), $patient, 'clinic', (int) $clinic->getId());
|
|
$this->em->persist($record);
|
|
$this->em->flush();
|
|
|
|
$case = new TreatmentCase('clinic', (int) $clinic->getId(), $record, $service, $protocol);
|
|
$this->em->persist($case);
|
|
$case->addArea(new TreatmentCaseArea($case, $categories['دست'], 0));
|
|
$case->addArea(new TreatmentCaseArea($case, $categories['پا'], 1));
|
|
foreach ([1, 2, 3] as $n) {
|
|
$case->addSession(new TreatmentSession($case, $n));
|
|
}
|
|
$this->em->flush();
|
|
|
|
return [$case, $clinic, $service, $categories];
|
|
}
|
|
|
|
private function sessionNumbers(TreatmentCase $case): array
|
|
{
|
|
$numbers = array_map(
|
|
static fn (TreatmentSession $s): int => $s->getSessionNumber(),
|
|
$case->getSessions()->toArray(),
|
|
);
|
|
sort($numbers);
|
|
|
|
return $numbers;
|
|
}
|
|
|
|
public function testStatusCanBeClosedAndReopened(): void
|
|
{
|
|
[$case] = $this->scenario();
|
|
|
|
$this->editor()->update($case, ['status' => TreatmentCase::STATUS_ABANDONED]);
|
|
self::assertSame(TreatmentCase::STATUS_ABANDONED, $case->getStatus());
|
|
self::assertNotNull($case->getClosedAt());
|
|
|
|
// بازگرداندن باید closedAt را پاک کند، وگرنه پرونده «بستهٔ فعال» میماند.
|
|
$this->editor()->update($case, ['status' => TreatmentCase::STATUS_ACTIVE]);
|
|
self::assertSame(TreatmentCase::STATUS_ACTIVE, $case->getStatus());
|
|
self::assertNull($case->getClosedAt());
|
|
}
|
|
|
|
public function testSupervisorCanBeChangedAndCleared(): void
|
|
{
|
|
[$case, $clinic] = $this->scenario();
|
|
|
|
$other = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر تازه');
|
|
$this->em->persist($other);
|
|
$clinic->getDoctors()->add($other);
|
|
$this->em->flush();
|
|
|
|
$this->editor()->update($case, ['supervisor_doctor_uuid' => $other->getUuid()]);
|
|
self::assertSame($other->getId(), $case->getSupervisorDoctor()?->getId());
|
|
|
|
$this->editor()->update($case, ['supervisor_doctor_uuid' => null]);
|
|
self::assertNull($case->getSupervisorDoctor());
|
|
}
|
|
|
|
public function testAreasAreReplaced(): void
|
|
{
|
|
[$case, , , $categories] = $this->scenario();
|
|
|
|
$this->editor()->update($case, ['area_uuids' => [
|
|
$categories['دست']->getUuid(),
|
|
$categories['صورت']->getUuid(),
|
|
]]);
|
|
|
|
$names = array_map(static fn ($a) => $a->getName(), $case->getAreas()->toArray());
|
|
sort($names);
|
|
self::assertSame(['دست', 'صورت'], $names);
|
|
}
|
|
|
|
/** حذف ناحیهای که جلسهای رویش ثبت شده یعنی پاک کردن سابقهٔ درمان. */
|
|
public function testAnAreaWithRecordsCannotBeRemoved(): void
|
|
{
|
|
[$case, , , $categories] = $this->scenario();
|
|
|
|
$area = $case->getAreas()->first();
|
|
$session = $case->getSessions()->first();
|
|
$this->em->persist(new \App\Treatment\Entity\SessionAreaRecord($session, $area));
|
|
$this->em->flush();
|
|
|
|
$this->expectException(\App\Shared\Exception\AppException::class);
|
|
$this->editor()->update($case, ['area_uuids' => [$categories['صورت']->getUuid()]]);
|
|
}
|
|
|
|
public function testSessionsGrowAndShrink(): void
|
|
{
|
|
[$case] = $this->scenario();
|
|
|
|
$this->editor()->update($case, ['total_sessions' => 5]);
|
|
self::assertSame([1, 2, 3, 4, 5], $this->sessionNumbers($case));
|
|
self::assertSame(5, $case->getTotalSessions());
|
|
|
|
$this->editor()->update($case, ['total_sessions' => 2]);
|
|
self::assertSame([1, 2], $this->sessionNumbers($case));
|
|
self::assertSame(2, $case->getTotalSessions());
|
|
}
|
|
|
|
/** جلسهای که نوبت گرفته کفِ تعداد را بالا میبرد؛ حذفش یعنی گم شدن یک نوبت واقعی. */
|
|
public function testSessionsCannotDropBelowBookedWork(): void
|
|
{
|
|
[$case, $clinic] = $this->scenario();
|
|
|
|
$sessions = $case->getSessions()->toArray();
|
|
usort($sessions, static fn ($a, $b) => $a->getSessionNumber() <=> $b->getSessionNumber());
|
|
|
|
$appointment = $this->newAppointment(
|
|
$clinic->getDoctors()->first(),
|
|
$this->createUser(['ROLE_USER']),
|
|
1_795_000_000,
|
|
1_795_001_800,
|
|
$clinic,
|
|
);
|
|
$this->em->persist($appointment);
|
|
$this->em->flush();
|
|
$sessions[2]->attachAppointment($appointment);
|
|
$this->em->flush();
|
|
|
|
$this->expectException(\App\Shared\Exception\AppException::class);
|
|
$this->editor()->update($case, ['total_sessions' => 2]);
|
|
}
|
|
|
|
public function testTotalSessionsIsBounded(): void
|
|
{
|
|
[$case] = $this->scenario();
|
|
|
|
$this->expectException(\App\Shared\Exception\AppException::class);
|
|
$this->editor()->update($case, ['total_sessions' => 1]);
|
|
}
|
|
|
|
/** جستجو باید همان کلیدی را بگیرد که منشی در فرم نوبت میزند. */
|
|
public function testSearchMatchesPatientAndService(): void
|
|
{
|
|
$name = 'نازنین ' . uniqid();
|
|
[$case, $clinic] = $this->scenario($name);
|
|
|
|
$type = 'clinic';
|
|
$id = (int) $clinic->getId();
|
|
|
|
self::assertCount(1, $this->cases()->findForTenant($type, $id, null, $name));
|
|
self::assertCount(1, $this->cases()->findForTenant($type, $id, null, 'لیزر بدن'));
|
|
self::assertSame([], $this->cases()->findForTenant($type, $id, null, 'چیزی که نیست'));
|
|
self::assertCount(1, $this->cases()->findForTenant($type, $id, null, null));
|
|
|
|
self::assertSame($case->getId(), $this->cases()->findForTenant($type, $id, null, $name)[0]->getId());
|
|
}
|
|
|
|
/** فیلتر بازه روی تاریخِ باز شدن پرونده است، نه سررسید جلسه. */
|
|
public function testDateRangeFiltersByOpenedAt(): void
|
|
{
|
|
[$case, $clinic] = $this->scenario('بازه ' . uniqid());
|
|
|
|
$type = 'clinic';
|
|
$id = (int) $clinic->getId();
|
|
$openedAt = $case->getOpenedAt();
|
|
|
|
// بازهای که همان لحظه را در بر میگیرد
|
|
self::assertCount(1, $this->cases()->findForTenant($type, $id, null, null, $openedAt - 60, $openedAt + 60));
|
|
|
|
// کرانِ پایین بعد از پرونده
|
|
self::assertSame([], $this->cases()->findForTenant($type, $id, null, null, $openedAt + 60, null));
|
|
|
|
// کرانِ بالا قبل از پرونده
|
|
self::assertSame([], $this->cases()->findForTenant($type, $id, null, null, null, $openedAt - 60));
|
|
|
|
// یکطرفه هم باید کار کند
|
|
self::assertCount(1, $this->cases()->findForTenant($type, $id, null, null, $openedAt - 60, null));
|
|
self::assertCount(1, $this->cases()->findForTenant($type, $id, null, null, null, $openedAt + 60));
|
|
}
|
|
}
|