feat: port clinic dashboard components from clinic-pro-tauri

- Add NewAppointmentsTable for displaying today's appointments with status chips and formatted time.
- Implement TauriCharts for bar and line charts representing patient counts and revenue.
- Create TauriDashboardView to combine stat cards, charts, and new appointments list.
- Introduce TauriStatCards for displaying key statistics with icons.
- Add dashboardIcons for SVG icons used in stat cards.
- Implement tests for DashboardPage to ensure correct rendering and API calls.
- Create DashboardTodayAppointmentsTest to validate extended fields in today's appointments API response.
This commit is contained in:
hamed
2026-07-14 13:54:50 +03:30
parent 3b9c5056a3
commit d9f96b68cd
10 changed files with 883 additions and 127 deletions
@@ -0,0 +1,130 @@
<?php
namespace App\Tests\Dashboard;
use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Tests\ApiTestCase;
/**
* GET /api/v1/dashboard/{doctor,clinic} — `today_appointments` rows must carry
* the extended fields (patient_mobile, doctor_name, service_name, slot_end)
* added for the ported «لیست نوبت‌های جدید» dashboard table.
*
* NOTE: db_test is never reset, so patient mobiles use the ApiTestCase random
* generator (never hardcoded) to avoid unique-constraint clashes across runs.
*/
class DashboardTodayAppointmentsTest extends ApiTestCase
{
private function todayAppointment(Doctor $doctor, User $patient): Appointment
{
// now() is guaranteed within [today midnight, tomorrow midnight-1]
$start = time();
$appt = new Appointment($doctor, $patient, $start, $start + 1_800);
$this->em->persist($appt);
$this->em->flush();
return $appt;
}
public function testDoctorTodayAppointmentsExposeExtendedFields(): void
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'حمیدی');
$patient = $this->createUser(['ROLE_USER']);
$section = new ServiceSection('doctor', 1, 'عمومی');
$item = new ServiceItem($section, 'ویزیت عمومی');
$this->em->persist($doctor);
$this->em->persist($section);
$this->em->persist($item);
$this->em->flush();
$appt = $this->todayAppointment($doctor, $patient);
$appt->setServiceItem($item);
$this->em->flush();
$res = $this->authJson('GET', '/api/v1/dashboard/doctor', $owner);
self::assertSame(200, $this->responseCode());
$rows = $res['data']['today_appointments'];
self::assertNotEmpty($rows, 'today_appointments should contain the booked slot');
$row = $rows[0];
self::assertSame($patient->getMobileNumber(), $row['patient_mobile']);
self::assertSame('حمیدی', $row['doctor_name']);
self::assertSame('ویزیت عمومی', $row['service_name']);
self::assertSame($appt->getSlotStart(), $row['slot_start']);
self::assertSame($appt->getSlotEnd(), $row['slot_end']);
self::assertArrayHasKey('status', $row);
}
public function testServiceNameNullWhenNoServiceItem(): void
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'حمیدی');
$this->em->persist($doctor);
$this->em->flush();
$patient = $this->createUser(['ROLE_USER']);
$this->todayAppointment($doctor, $patient);
$res = $this->authJson('GET', '/api/v1/dashboard/doctor', $owner);
self::assertSame(200, $this->responseCode());
$row = $res['data']['today_appointments'][0];
self::assertNull($row['service_name'], 'service_name is null when no service item is booked');
self::assertSame($patient->getMobileNumber(), $row['patient_mobile']);
}
public function testClinicTodayAppointmentsExposeExtendedFields(): void
{
$clinicOwner = $this->createUser(['ROLE_CLINIC']);
$clinic = new Clinic($clinicOwner);
$clinic->setName('کلینیک نمونه');
$docOwner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($docOwner, 'حمیدی');
$clinic->getDoctors()->add($doctor);
$section = new ServiceSection('doctor', 1, 'عمومی');
$item = new ServiceItem($section, 'ویزیت عمومی');
$patient = $this->createUser(['ROLE_USER']);
$this->em->persist($doctor);
$this->em->persist($clinic);
$this->em->persist($section);
$this->em->persist($item);
$this->em->flush();
$appt = $this->todayAppointment($doctor, $patient);
$appt->setServiceItem($item);
$this->em->flush();
$res = $this->authJson('GET', '/api/v1/dashboard/clinic', $clinicOwner);
self::assertSame(200, $this->responseCode());
$rows = $res['data']['today_appointments'];
self::assertNotEmpty($rows);
$row = $rows[0];
self::assertSame($patient->getMobileNumber(), $row['patient_mobile']);
self::assertSame('حمیدی', $row['doctor_name']);
self::assertSame('ویزیت عمومی', $row['service_name']);
self::assertSame($appt->getSlotEnd(), $row['slot_end']);
}
public function testDoctorNotFoundReturns404(): void
{
// ROLE_DOCTOR user without a Doctor entity
$user = $this->createUser(['ROLE_DOCTOR']);
$res = $this->authJson('GET', '/api/v1/dashboard/doctor', $user);
self::assertSame(404, $this->responseCode());
self::assertFalse($res['success']);
}
}