feat: port نوبتها (appointments) tab from tauri to patient detail page
Replace the placeholder row list on the patient detail «نوبتها» tab with the
card-grid design ported pixel-for-pixel from clinic-pro-tauri TurnsSection:
- New AppointmentTurnCard mirrors tauri TurnsCard (success icon, title, date/time
chips, personnel, status). Status uses the live AppointmentStatusDropdown
instead of the tauri mock.
- New AppointmentsTab in PatientDetailPage: sort/filter toolbar (client-side) +
reserve/new buttons linking to existing /admin/appointments pages + card grid.
- Add CalendarD/ClockP/UserD/StatusGlobe icons (verbatim from tauri).
- Backend: expose version on GET /patient/{uuid}/appointments so the status
dropdown can optimistic-lock. No new endpoint.
- Tests: PatientAppointmentsTest (shape/version/order/empty/ownership) +
AppointmentTurnCard + tab data/empty cases. docs/api/patient.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* List Patient Appointments (تب نوبتها): shape (incl. version), ordering,
|
||||
* empty boundary, and ownership scoping.
|
||||
*/
|
||||
class PatientAppointmentsTest extends ApiTestCase
|
||||
{
|
||||
/** @return array{0: \App\Auth\Entity\User, 1: Doctor, 2: PatientRecord} */
|
||||
private function recordFor(): array
|
||||
{
|
||||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||
$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, $doctor, $record];
|
||||
}
|
||||
|
||||
private function appointment(Doctor $doctor, PatientRecord $record, int $slotStart): Appointment
|
||||
{
|
||||
$appt = new Appointment($doctor, $record->getUser(), $slotStart, $slotStart + 1800);
|
||||
$this->em->persist($appt);
|
||||
$this->em->flush();
|
||||
|
||||
return $appt;
|
||||
}
|
||||
|
||||
public function testListReturnsShapeWithVersion(): void
|
||||
{
|
||||
[$owner, $doctor, $record] = $this->recordFor();
|
||||
$this->appointment($doctor, $record, 1_754_000_000);
|
||||
|
||||
$list = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/appointments', $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertCount(1, $list['data']);
|
||||
|
||||
$row = $list['data'][0];
|
||||
foreach (['uuid', 'starts_at', 'ends_at', 'status', 'version', 'doctor_name'] as $key) {
|
||||
self::assertArrayHasKey($key, $row);
|
||||
}
|
||||
self::assertSame(1, $row['version']);
|
||||
self::assertSame('pending', $row['status']);
|
||||
self::assertSame('دکتر ژیلا فتحی', $row['doctor_name']);
|
||||
self::assertSame(1_754_000_000, $row['starts_at']);
|
||||
}
|
||||
|
||||
public function testSortedByStartDescending(): void
|
||||
{
|
||||
[$owner, $doctor, $record] = $this->recordFor();
|
||||
$this->appointment($doctor, $record, 1_754_000_000);
|
||||
$this->appointment($doctor, $record, 1_755_000_000);
|
||||
|
||||
$list = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/appointments', $owner);
|
||||
self::assertCount(2, $list['data']);
|
||||
self::assertSame(1_755_000_000, $list['data'][0]['starts_at']);
|
||||
self::assertSame(1_754_000_000, $list['data'][1]['starts_at']);
|
||||
}
|
||||
|
||||
public function testEmptyWhenNoAppointments(): void
|
||||
{
|
||||
[$owner, , $record] = $this->recordFor();
|
||||
|
||||
$list = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/appointments', $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertCount(0, $list['data']);
|
||||
}
|
||||
|
||||
public function testNotFoundForUnknownRecord(): void
|
||||
{
|
||||
[$owner] = $this->recordFor();
|
||||
$this->authJson('GET', '/api/v1/patient/00000000-0000-0000-0000-000000000000/appointments', $owner);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testOwnershipScoped(): void
|
||||
{
|
||||
[, $doctor, $record] = $this->recordFor();
|
||||
$this->appointment($doctor, $record, 1_754_000_000);
|
||||
|
||||
[$other] = $this->recordFor();
|
||||
$this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/appointments', $other);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user