feat(patients): phase A — records list + create/edit form (Figma)
Rebuild the patient records (پروندهها) area, phase A of the Figma redesign:
- BE: add a clinic-scoped `record_number` and a TenantTag `tags` M2M to
PatientRecord (migration + EAGER-hydrated collection). POST /patient and
PATCH /patient/{uuid} now accept `record_number` and tenant-scoped `tags`
(foreign tag → 422); demographic fields (gender, date_of_birth,
referral_source, description) continue to live on UserProfile via PATCH.
- FE: new PatientsListPage (table + card views, search, pagination, tags
column, "تشکیل پرونده") at /admin/patients, and PatientRecordFormPage
(create/edit) that POSTs the record then PATCHes the demographics. Point
the sidebar "پرونده" entry to the new list.
Phases B–E (tabbed patient file, service stepper, invoice, payments/wallet,
call-center) follow. Backend covered by PHPUnit, FE by Vitest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Repository\PatientRecordRepository;
|
||||
use App\Tag\Entity\TenantTag;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* PatientRecord carries clinic-scoped case-file fields (record_number, gender,
|
||||
* birth_date, referral_source, description) and a set of TenantTag labels.
|
||||
*/
|
||||
class PatientRecordFieldsTest extends ApiTestCase
|
||||
{
|
||||
public function testFieldsAndTagsRoundTrip(): void
|
||||
{
|
||||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($owner, 'دکتر');
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
$t1 = new TenantTag('doctor', $doctor->getId(), 'فوری', '#FF0000');
|
||||
$t2 = new TenantTag('doctor', $doctor->getId(), 'پیگیری', '#00AA00');
|
||||
$this->em->persist($t1);
|
||||
$this->em->persist($t2);
|
||||
$this->em->flush();
|
||||
|
||||
$patient = $this->createUser(['ROLE_USER']); // random unique mobile — db_test is never reset
|
||||
$patient->setRealName('بیمار نمونه');
|
||||
$this->em->flush();
|
||||
|
||||
$record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
|
||||
$record->setRecordNumber('P-1001')
|
||||
->setTags([$t1, $t2]);
|
||||
$this->em->persist($record);
|
||||
$this->em->flush();
|
||||
$uuid = $record->getUuid();
|
||||
$this->em->clear();
|
||||
|
||||
/** @var PatientRecordRepository $repo */
|
||||
$repo = static::getContainer()->get(PatientRecordRepository::class);
|
||||
$reloaded = $repo->findByUuid($uuid);
|
||||
self::assertNotNull($reloaded);
|
||||
self::assertCount(2, $reloaded->getTags());
|
||||
|
||||
$arr = $reloaded->toArray();
|
||||
self::assertSame('P-1001', $arr['record_number']);
|
||||
self::assertCount(2, $arr['tags']);
|
||||
self::assertSame('فوری', $arr['tags'][0]['name']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Tag\Entity\TenantTag;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* POST/PATCH /patient accept `record_number` and tenant-scoped `tags` (uuids).
|
||||
*/
|
||||
class PatientRecordTagsApiTest 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];
|
||||
}
|
||||
|
||||
private function tag(int $doctorId, string $name): TenantTag
|
||||
{
|
||||
$t = new TenantTag('doctor', $doctorId, $name, '#FF0000');
|
||||
$this->em->persist($t);
|
||||
$this->em->flush();
|
||||
return $t;
|
||||
}
|
||||
|
||||
public function testCreateAndUpdateWithRecordNumberAndTags(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$t1 = $this->tag($doctor->getId(), 'فوری');
|
||||
$t2 = $this->tag($doctor->getId(), 'پیگیری');
|
||||
|
||||
$mobile = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
|
||||
$created = $this->authJson('POST', '/api/v1/patient', $owner, [
|
||||
'mobile' => $mobile,
|
||||
'name' => 'بیمار نمونه',
|
||||
'record_number' => 'P-1001',
|
||||
'tags' => [$t1->getUuid(), $t2->getUuid()],
|
||||
]);
|
||||
self::assertSame(201, $this->responseCode());
|
||||
self::assertSame('P-1001', $created['data']['record_number']);
|
||||
self::assertCount(2, $created['data']['tags']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
// update: keep one tag, change record number
|
||||
$updated = $this->authJson('PATCH', '/api/v1/patient/' . $uuid, $owner, [
|
||||
'record_number' => 'P-2002',
|
||||
'tags' => [$t1->getUuid()],
|
||||
]);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('P-2002', $updated['data']['record_number']);
|
||||
self::assertCount(1, $updated['data']['tags']);
|
||||
}
|
||||
|
||||
public function testRejectsForeignTag(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->doctor();
|
||||
$mobile = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
|
||||
$created = $this->authJson('POST', '/api/v1/patient', $owner, ['mobile' => $mobile, 'name' => 'ب']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
[, $doctorB] = $this->doctor();
|
||||
$foreign = $this->tag($doctorB->getId(), 'خارجی');
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/patient/' . $uuid, $owner, ['tags' => [$foreign->getUuid()]]);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user