feat(treatment): per-case operators, shown on the list and searchable
A treatment case said which doctor supervised it but never who actually did the work, so the list could not answer the first question a manager asks about a course: who performed it. Two separate things now travel with the case. `performed_by` is history — derived from the sessions' performedBy, so it only ever reports what happened. `assigned_staff` is plan — a new treatment_case_staff table, editable from the modal, saying who is meant to handle this patient's course. The card shows the first and falls back to the second while nothing has been performed yet. Search matches both. A manager typing an operator's name wants that person's work, and work already done is part of it. Assignment also narrows the operator queue: a case with assigned staff shows its sessions only to those people, because a patient who started a multi-session course with one operator should keep them. An unassigned case keeps the existing protocol rule, and an empty list means "anyone the protocol allows" rather than "nobody" — the same "no rows is not a restriction" convention used elsewhere. Unlike areas, removing an operator erases nothing: a finished session carries its real operator on itself and never consults this list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ use App\Doctor\Entity\Doctor;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Tests\ApiTestCase;
|
||||
use App\Treatment\Entity\TreatmentCase;
|
||||
use App\Staff\Entity\ClinicStaff;
|
||||
use App\Treatment\Entity\TreatmentCaseArea;
|
||||
use App\Treatment\Entity\TreatmentProtocol;
|
||||
use App\Treatment\Entity\TreatmentProtocolStep;
|
||||
@@ -219,6 +220,85 @@ class TreatmentCaseEditTest extends ApiTestCase
|
||||
self::assertSame($case->getId(), $this->cases()->findForTenant($type, $id, null, $name)[0]->getId());
|
||||
}
|
||||
|
||||
private function staffIn(Clinic $clinic, string $name): ClinicStaff
|
||||
{
|
||||
$staff = new ClinicStaff('clinic', (int) $clinic->getId(), $name);
|
||||
$this->em->persist($staff);
|
||||
$this->em->flush();
|
||||
|
||||
return $staff;
|
||||
}
|
||||
|
||||
/** اپراتورِ پرونده: بیمار دورهاش را با یک نفر شروع میکند. */
|
||||
public function testAssignedStaffCanBeSetAndCleared(): void
|
||||
{
|
||||
[$case, $clinic] = $this->scenario();
|
||||
$a = $this->staffIn($clinic, 'اپراتور الف');
|
||||
$b = $this->staffIn($clinic, 'اپراتور ب');
|
||||
|
||||
$this->editor()->update($case, ['staff_uuids' => [$a->getUuid(), $b->getUuid()]]);
|
||||
|
||||
$names = array_map(static fn ($r) => $r->getStaff()->getFullName(), $case->getAssignedStaff()->toArray());
|
||||
sort($names);
|
||||
self::assertSame(['اپراتور الف', 'اپراتور ب'], $names);
|
||||
|
||||
// فهرست خالی مجاز است — یعنی «هر کسی که پروتکل مجاز دانسته»، نه «هیچکس».
|
||||
$this->editor()->update($case, ['staff_uuids' => []]);
|
||||
self::assertCount(0, $case->getAssignedStaff());
|
||||
}
|
||||
|
||||
public function testStaffFromAnotherTenantIsRejected(): void
|
||||
{
|
||||
[$case] = $this->scenario();
|
||||
|
||||
$otherClinic = new Clinic($this->createUser(['ROLE_CLINIC']));
|
||||
$otherClinic->setName('کلینیک دیگر ' . uniqid());
|
||||
$this->em->persist($otherClinic);
|
||||
$this->em->flush();
|
||||
$outsider = $this->staffIn($otherClinic, 'پرسنل بیرونی');
|
||||
|
||||
$this->expectException(\App\Shared\Exception\AppException::class);
|
||||
$this->editor()->update($case, ['staff_uuids' => [$outsider->getUuid()]]);
|
||||
}
|
||||
|
||||
/** مدیر نام اپراتور را میزند و انتظار دارد پروندههایش بیاید. */
|
||||
public function testSearchMatchesAssignedStaff(): void
|
||||
{
|
||||
[$case, $clinic] = $this->scenario();
|
||||
$name = 'اپراتور ' . uniqid();
|
||||
$staff = $this->staffIn($clinic, $name);
|
||||
|
||||
$type = 'clinic';
|
||||
$id = (int) $clinic->getId();
|
||||
|
||||
self::assertSame([], $this->cases()->findForTenant($type, $id, null, $name));
|
||||
|
||||
$this->editor()->update($case, ['staff_uuids' => [$staff->getUuid()]]);
|
||||
|
||||
$found = $this->cases()->findForTenant($type, $id, null, $name);
|
||||
self::assertCount(1, $found);
|
||||
self::assertSame($case->getId(), $found[0]->getId());
|
||||
}
|
||||
|
||||
/** «کارهای این نفر» شاملِ کارِ انجامشده هم هست، نه فقط اختصاصِ آینده. */
|
||||
public function testSearchMatchesTheOperatorWhoPerformedASession(): void
|
||||
{
|
||||
[$case, $clinic] = $this->scenario();
|
||||
$name = 'انجامدهنده ' . uniqid();
|
||||
$staff = $this->staffIn($clinic, $name);
|
||||
|
||||
$case->getSessions()->first()->setPerformedBy($staff);
|
||||
$this->em->flush();
|
||||
|
||||
$found = $this->cases()->findForTenant('clinic', (int) $clinic->getId(), null, $name);
|
||||
self::assertCount(1, $found);
|
||||
|
||||
// و در payload هم دیده میشود، جدا از اختصاص.
|
||||
$payload = $found[0]->toArray();
|
||||
self::assertSame([$name], array_column($payload['performed_by'], 'name'));
|
||||
self::assertSame([], $payload['assigned_staff']);
|
||||
}
|
||||
|
||||
/** فیلتر بازه روی تاریخِ باز شدن پرونده است، نه سررسید جلسه. */
|
||||
public function testDateRangeFiltersByOpenedAt(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user