feat: replace patient messages tab with pinnable notes
Port the tauri AddNoteModal notes feature into the admin patient case-file,
replacing the mislabeled «پیامها» (SMS log) tab with «یادداشتها».
Backend (src/Patient):
- PatientNote entity + repository (record-scoped, pinned-first ordering)
- CRUD endpoints on PatientController: GET /notes, POST /note,
PATCH /note/{uuid} (edit body + toggle pin), DELETE /note/{uuid}
- author display name captured server-side from the current user
- migration for patient_notes; docs/api/patient.md updated
Frontend (assets/admin):
- NotesTab: compose box, newest/oldest sort, pinned-first list with
accent rail + pin/edit/delete, edit modal, confirm-delete, empty state
- tab key/label/icon messages -> notes; onAddNote deep-links the notes tab
Tests: PatientNoteTest (create/list-order/edit/pin/delete/validation/ownership),
PatientDetailPage notes cases (render/empty/pin/create).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* Patient notes: create, list (pinned-first order), edit, pin toggle, delete,
|
||||
* validation and tenant ownership scoping.
|
||||
*/
|
||||
class PatientNoteTest extends ApiTestCase
|
||||
{
|
||||
/** @return array{0: \App\Auth\Entity\User, 1: PatientRecord} */
|
||||
private function recordFor(): array
|
||||
{
|
||||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||
$owner->setRealName('دکتر احمدی');
|
||||
$doctor = new Doctor($owner, 'دکتر');
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
$patient = $this->createUser(['ROLE_USER']);
|
||||
$record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
|
||||
$this->em->persist($record);
|
||||
$this->em->flush();
|
||||
|
||||
return [$owner, $record];
|
||||
}
|
||||
|
||||
public function testCreateCapturesAuthorAndDefaults(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
|
||||
$created = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/note', $owner, [
|
||||
'body' => 'بیمار به دارو حساسیت دارد',
|
||||
]);
|
||||
|
||||
self::assertSame(201, $this->responseCode());
|
||||
self::assertSame('بیمار به دارو حساسیت دارد', $created['data']['body']);
|
||||
self::assertFalse($created['data']['pinned']);
|
||||
self::assertSame('دکتر احمدی', $created['data']['author']);
|
||||
self::assertNull($created['data']['updated_at']);
|
||||
}
|
||||
|
||||
public function testListReturnsPinnedFirst(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
$base = '/api/v1/patient/' . $record->getUuid();
|
||||
|
||||
// Three notes; the oldest is pinned so it must float above the two newer ones.
|
||||
$pinned = $this->authJson('POST', $base . '/note', $owner, ['body' => 'قدیمی پینشده', 'pinned' => true]);
|
||||
$this->authJson('POST', $base . '/note', $owner, ['body' => 'دوم']);
|
||||
$this->authJson('POST', $base . '/note', $owner, ['body' => 'سوم (جدیدترین)']);
|
||||
|
||||
$list = $this->authJson('GET', $base . '/notes', $owner);
|
||||
self::assertCount(3, $list['data']);
|
||||
self::assertSame($pinned['data']['uuid'], $list['data'][0]['uuid']);
|
||||
self::assertTrue($list['data'][0]['pinned']);
|
||||
// remaining two are newest-first among the unpinned
|
||||
self::assertSame('سوم (جدیدترین)', $list['data'][1]['body']);
|
||||
self::assertSame('دوم', $list['data'][2]['body']);
|
||||
}
|
||||
|
||||
public function testEditBodyAndTogglePin(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
$created = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/note', $owner, ['body' => 'اولیه']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
$edited = $this->authJson('PATCH', '/api/v1/patient/note/' . $uuid, $owner, [
|
||||
'body' => 'ویرایش شد', 'pinned' => true,
|
||||
]);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('ویرایش شد', $edited['data']['body']);
|
||||
self::assertTrue($edited['data']['pinned']);
|
||||
self::assertNotNull($edited['data']['updated_at']);
|
||||
|
||||
// pin-only toggle keeps body intact
|
||||
$unpinned = $this->authJson('PATCH', '/api/v1/patient/note/' . $uuid, $owner, ['pinned' => false]);
|
||||
self::assertFalse($unpinned['data']['pinned']);
|
||||
self::assertSame('ویرایش شد', $unpinned['data']['body']);
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
$created = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/note', $owner, ['body' => 'برای حذف']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
$this->authJson('DELETE', '/api/v1/patient/note/' . $uuid, $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$after = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/notes', $owner);
|
||||
self::assertCount(0, $after['data']);
|
||||
}
|
||||
|
||||
public function testRequiresBodyOnCreate(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
$this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/note', $owner, ['body' => ' ']);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testRejectsEmptyBodyOnEdit(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
$created = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/note', $owner, ['body' => 'اولیه']);
|
||||
$this->authJson('PATCH', '/api/v1/patient/note/' . $created['data']['uuid'], $owner, ['body' => '']);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testOwnershipScoped(): void
|
||||
{
|
||||
[$owner, $record] = $this->recordFor();
|
||||
$created = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/note', $owner, ['body' => 'x']);
|
||||
$uuid = $created['data']['uuid'];
|
||||
|
||||
[$other] = $this->recordFor();
|
||||
$this->authJson('PATCH', '/api/v1/patient/note/' . $uuid, $other, ['pinned' => true]);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
$this->authJson('DELETE', '/api/v1/patient/note/' . $uuid, $other);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user