Files
clinicpro/tests/Treatment/TreatmentCaseEditTest.php
T
hamedandClaude Opus 5 73963020e2 feat(treatment): date-range filter on treatment cases, and time on the start stamp
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>
2026-08-07 13:57:14 +03:30

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));
}
}