feat(patients): inline tag popover + advanced filters on the records list
Port the remaining pieces of tauri /files list into /admin/patients:
- inline tag assignment: the برچسبها cell (table + card) opens a popover to
assign/remove tenant tags without leaving the list. Uses existing endpoints
(GET /api/v1/tenant-tags + PATCH /api/v1/patient/{uuid} { tags:[uuid] }).
New component assets/admin/components/PatientTagsCell.tsx.
- advanced filter modal (PatientsFilterModal): admission date range, insurance,
service status (pending/completed), has-debt, gender, tags — wired to the
list query with an active-filter badge on the button.
Backend: GET /api/v1/patients gains tags/gender/insurance_id/admitted_from/
admitted_to/service_status/has_debt filters via a shared applyFilters() on
PatientRecordRepository (findByEntity + countByEntity stay consistent). Debt
and service status derive from unpaid sessions (payment_method='pending'),
documented in docs/api/patient.md.
Tests: tests/Patient/PatientListFilterTest.php (5) + PatientsListPage tag-popover
and filter-apply tests. Pre-existing LoginPage.test failures are unrelated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Entity\PatientSession;
|
||||
use App\Tag\Entity\TenantTag;
|
||||
use App\Tests\ApiTestCase;
|
||||
use App\UserProfile\Entity\UserProfile;
|
||||
|
||||
/**
|
||||
* GET /api/v1/patients advanced filters: tags, gender, insurance, admission
|
||||
* date range, service status (pending/completed) and has-debt.
|
||||
*/
|
||||
class PatientListFilterTest extends ApiTestCase
|
||||
{
|
||||
private function doctor(): array
|
||||
{
|
||||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($owner, 'دکتر تست');
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
return [$owner, $doctor];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{gender?:string,insurance?:int,tag?:TenantTag,pending?:bool,paid?:bool,createdAt?:int} $opts
|
||||
*/
|
||||
private function record(Doctor $doctor, array $opts = []): PatientRecord
|
||||
{
|
||||
$patient = $this->createUser(['ROLE_USER']);
|
||||
$record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
|
||||
if (isset($opts['createdAt'])) {
|
||||
$ref = new \ReflectionProperty($record, 'createdAt');
|
||||
$ref->setAccessible(true);
|
||||
$ref->setValue($record, $opts['createdAt']);
|
||||
}
|
||||
$this->em->persist($record);
|
||||
|
||||
if (isset($opts['gender']) || isset($opts['insurance'])) {
|
||||
$profile = new UserProfile($patient);
|
||||
if (isset($opts['gender'])) $profile->setGender($opts['gender']);
|
||||
if (isset($opts['insurance'])) $profile->setBasicInsuranceId($opts['insurance']);
|
||||
$this->em->persist($profile);
|
||||
}
|
||||
if (isset($opts['tag'])) {
|
||||
$record->getTags()->add($opts['tag']);
|
||||
}
|
||||
if (!empty($opts['pending']) || !empty($opts['paid'])) {
|
||||
$session = new PatientSession($record);
|
||||
$session->setPaymentMethod(!empty($opts['pending']) ? 'pending' : 'cash');
|
||||
$this->em->persist($session);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function tag(Doctor $doctor, string $name): TenantTag
|
||||
{
|
||||
$tag = new TenantTag('doctor', $doctor->getId(), $name, '#5559CE');
|
||||
$this->em->persist($tag);
|
||||
$this->em->flush();
|
||||
return $tag;
|
||||
}
|
||||
|
||||
private function uuids(array $res): array
|
||||
{
|
||||
return array_map(static fn(array $r) => $r['uuid'], $res['data']);
|
||||
}
|
||||
|
||||
public function testFilterByTag(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$tag = $this->tag($doctor, 'خوشحساب');
|
||||
$tagged = $this->record($doctor, ['tag' => $tag]);
|
||||
$this->record($doctor); // untagged
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/patients?tags=' . $tag->getUuid(), $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame(1, $res['meta']['totalRecords']);
|
||||
self::assertSame($tagged->getUuid(), $res['data'][0]['uuid']);
|
||||
}
|
||||
|
||||
public function testFilterByGenderAndInsurance(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$male = $this->record($doctor, ['gender' => 'male', 'insurance' => 7]);
|
||||
$this->record($doctor, ['gender' => 'female', 'insurance' => 9]);
|
||||
|
||||
$byGender = $this->authJson('GET', '/api/v1/patients?gender=male', $owner);
|
||||
self::assertSame(1, $byGender['meta']['totalRecords']);
|
||||
self::assertSame($male->getUuid(), $byGender['data'][0]['uuid']);
|
||||
|
||||
$byIns = $this->authJson('GET', '/api/v1/patients?insurance_id=7', $owner);
|
||||
self::assertSame(1, $byIns['meta']['totalRecords']);
|
||||
self::assertSame($male->getUuid(), $byIns['data'][0]['uuid']);
|
||||
}
|
||||
|
||||
public function testFilterByAdmissionDateRange(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$old = $this->record($doctor, ['createdAt' => 1000]);
|
||||
$new = $this->record($doctor, ['createdAt' => 5000]);
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/patients?admitted_from=4000&admitted_to=6000', $owner);
|
||||
self::assertSame(1, $res['meta']['totalRecords']);
|
||||
self::assertSame($new->getUuid(), $res['data'][0]['uuid']);
|
||||
self::assertNotContains($old->getUuid(), $this->uuids($res));
|
||||
}
|
||||
|
||||
public function testFilterByServiceStatusAndDebt(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$pending = $this->record($doctor, ['pending' => true]);
|
||||
$completed = $this->record($doctor, ['paid' => true]);
|
||||
|
||||
$onlyPending = $this->authJson('GET', '/api/v1/patients?service_status=pending', $owner);
|
||||
self::assertSame(1, $onlyPending['meta']['totalRecords']);
|
||||
self::assertSame($pending->getUuid(), $onlyPending['data'][0]['uuid']);
|
||||
|
||||
$onlyCompleted = $this->authJson('GET', '/api/v1/patients?service_status=completed', $owner);
|
||||
self::assertSame(1, $onlyCompleted['meta']['totalRecords']);
|
||||
self::assertSame($completed->getUuid(), $onlyCompleted['data'][0]['uuid']);
|
||||
|
||||
$withDebt = $this->authJson('GET', '/api/v1/patients?has_debt=1', $owner);
|
||||
self::assertSame(1, $withDebt['meta']['totalRecords']);
|
||||
self::assertSame($pending->getUuid(), $withDebt['data'][0]['uuid']);
|
||||
}
|
||||
|
||||
public function testNoFiltersReturnsAll(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$this->record($doctor);
|
||||
$this->record($doctor);
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/patients', $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame(2, $res['meta']['totalRecords']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user