feat: add patient appointment interface and endpoints

- Introduced `PatientAppointment` interface to define appointment structure.
- Implemented `findByUserAndDoctorIds` method in `AppointmentRepository` to retrieve appointments for a user filtered by doctor IDs.
- Added `acceptedDoctorIdsByClinic` method in `ClinicDoctorInvitationRepository` to get accepted doctor IDs for a clinic.
- Created new endpoint in `PatientController` to fetch appointments for a patient, ensuring only relevant doctors' appointments are displayed.
This commit is contained in:
hamed
2026-07-13 10:15:44 +03:30
parent 5c09fe8ac3
commit a4a17bf8c7
5 changed files with 564 additions and 370 deletions
File diff suppressed because it is too large Load Diff
+11
View File
@@ -502,6 +502,17 @@ export interface SessionServiceLine {
created_at: number;
}
export interface PatientAppointment {
uuid: string;
starts_at: number;
ends_at: number | null;
status: string;
doctor_name: string | null;
service_name: string | null;
price_rials: number | null;
created_at: number;
}
export interface PatientSession {
uuid: string;
record_uuid: string;
@@ -128,6 +128,29 @@ class AppointmentRepository extends ServiceEntityRepository
return $this->findBy($criteria, ['slotStart' => 'DESC']);
}
/**
* نوبت‌های یک بیمار (کاربر) که با پزشک(های) مشخص گرفته شده‌اند — برای نمایش در
* پروندهٔ بیمار. اگر لیست پزشک خالی باشد، آرایهٔ خالی برمی‌گرداند.
*
* @param int[] $doctorIds
* @return Appointment[]
*/
public function findByUserAndDoctorIds(User $user, array $doctorIds): array
{
if ($doctorIds === []) {
return [];
}
return $this->createQueryBuilder('a')
->where('a.user = :user')
->andWhere('a.doctor IN (:doctorIds)')
->setParameter('user', $user)
->setParameter('doctorIds', $doctorIds)
->orderBy('a.slotStart', 'DESC')
->getQuery()
->getResult();
}
/** Whether the user had a confirmed appointment with this doctor within the last $sinceDays days. */
public function hasRecentConfirmed(User $user, Doctor $doctor, int $sinceDays = 30): bool
{
@@ -46,6 +46,26 @@ class ClinicDoctorInvitationRepository extends ServiceEntityRepository
->getResult();
}
/**
* شناسهٔ پزشکانی که دعوت پذیرفته‌شده در این کلینیک دارند.
*
* @return int[]
*/
public function acceptedDoctorIdsByClinic(int $clinicId): array
{
$rows = $this->createQueryBuilder('i')
->select('IDENTITY(i.doctor) AS doctorId')
->where('i.clinic = :clinicId')
->andWhere('i.status = :accepted')
->andWhere('i.doctor IS NOT NULL')
->setParameter('clinicId', $clinicId)
->setParameter('accepted', ClinicDoctorInvitation::STATUS_ACCEPTED)
->getQuery()
->getScalarResult();
return array_map(static fn(array $r) => (int) $r['doctorId'], $rows);
}
public function save(ClinicDoctorInvitation $invitation): void
{
$em = $this->getEntityManager();
@@ -47,6 +47,8 @@ class PatientController extends BaseController
private readonly InvoiceService $invoiceService,
private readonly ClaimService $claimService,
private readonly InvoiceRepository $invoiceRepo,
private readonly \App\Appointment\Repository\AppointmentRepository $appointmentRepo,
private readonly \App\ClinicInvitation\Repository\ClinicDoctorInvitationRepository $invitationRepo,
private readonly LoggerInterface $logger,
) {}
@@ -318,6 +320,37 @@ class PatientController extends BaseController
);
}
#[Route('/api/v1/patient/{uuid}/appointments', methods: ['GET'])]
public function appointments(string $uuid, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$this->assertPatientGate($entityType, $entityId);
$record = $this->recordRepo->findByUuid($uuid);
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
}
// نوبت‌های این بیمار فقط با پزشک(های) همین ارائه‌دهنده نمایش داده می‌شوند تا
// نوبت‌های او با کلینیک‌های دیگر نشت نکند.
$doctorIds = $entityType === 'doctor'
? [$entityId]
: $this->invitationRepo->acceptedDoctorIdsByClinic($entityId);
$appointments = $this->appointmentRepo->findByUserAndDoctorIds($record->getUser(), $doctorIds);
return $this->success(array_map(fn(\App\Appointment\Entity\Appointment $a) => [
'uuid' => $a->getUuid(),
'starts_at' => $a->getSlotStart(),
'ends_at' => $a->getSlotEnd(),
'status' => $a->getStatus(),
'doctor_name' => $a->getDoctor()->getName(),
'service_name' => null,
'price_rials' => null,
'created_at' => $a->getSlotStart(),
], $appointments));
}
/**
* خروجی session به‌همراه خلاصه‌ی صورتحساب: uuid فاکتور (در صورت وجود) و
* مانده‌ی بدهیِ سهم بیمار. اگر session تسویه شده باشد (payment_method != pending)