248 lines
11 KiB
PHP
248 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Appointment\Controller;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Auth\Entity\User;
|
|
use App\Clinic\Repository\ClinicRepository;
|
|
use App\Doctor\Repository\DoctorRepository;
|
|
use App\Secretary\Repository\DoctorSecretaryRepository;
|
|
use App\Shared\Controller\BaseController;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
class MyAppointmentsController extends BaseController
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly DoctorRepository $doctorRepo,
|
|
private readonly ClinicRepository $clinicRepo,
|
|
private readonly DoctorSecretaryRepository $secretaryRepo,
|
|
) {}
|
|
|
|
#[Route('/api/v1/my/appointment', methods: ['POST'])]
|
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
|
public function createAppointment(Request $request, #[CurrentUser] User $user): JsonResponse
|
|
{
|
|
$roles = $user->getRoles();
|
|
$allowed = ['ROLE_DOCTOR', 'ROLE_CLINIC', 'ROLE_SECRETARY', 'ROLE_ADMIN'];
|
|
if (!array_intersect($allowed, $roles)) {
|
|
return $this->error('FORBIDDEN', 'دسترسی ندارید', 403);
|
|
}
|
|
|
|
$data = json_decode($request->getContent(), true) ?? [];
|
|
$doctorUuid = trim($data['doctor_uuid'] ?? '');
|
|
$slotStart = (int) ($data['slot_start'] ?? 0);
|
|
$slotEnd = (int) ($data['slot_end'] ?? 0);
|
|
$mobile = trim($data['patient_mobile'] ?? '');
|
|
$patientName = trim($data['patient_name'] ?? '');
|
|
|
|
if (empty($doctorUuid) || $slotStart <= 0 || $slotEnd <= $slotStart || empty($mobile) || empty($patientName)) {
|
|
return $this->error('VALIDATION', 'همه فیلدها الزامی است', 422);
|
|
}
|
|
|
|
if ($slotStart < time()) {
|
|
return $this->error('SLOT_PAST', 'زمان این اسلات گذشته است', 422);
|
|
}
|
|
|
|
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
|
if (!$doctor) return $this->error('DOCTOR_NOT_FOUND', 'پزشک یافت نشد', 404);
|
|
|
|
$patient = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
|
|
if (!$patient) {
|
|
$patient = new User($mobile);
|
|
$patient->setRealName($patientName);
|
|
$patient->setRoles(['ROLE_USER']);
|
|
$this->em->persist($patient);
|
|
}
|
|
|
|
$conflict = $this->em->createQueryBuilder()
|
|
->select('COUNT(a.id)')->from(Appointment::class, 'a')
|
|
->where('a.doctor = :doctor')
|
|
->andWhere('a.slotStart < :end AND a.slotEnd > :start')
|
|
->andWhere("a.status NOT IN ('cancelled_by_doctor','cancelled_by_user','cancelled_by_admin','auto_cancel_unpaid')")
|
|
->setParameter('doctor', $doctor)
|
|
->setParameter('start', $slotStart)
|
|
->setParameter('end', $slotEnd)
|
|
->getQuery()->getSingleScalarResult();
|
|
|
|
if ($conflict > 0) return $this->error('SLOT_TAKEN', 'این نوبت قبلاً رزرو شده است', 409);
|
|
|
|
$appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
|
|
if (!empty($data['note'])) $appointment->setNote($data['note']);
|
|
$this->em->persist($appointment);
|
|
$this->em->flush();
|
|
|
|
return $this->success([
|
|
'uuid' => $appointment->getUuid(),
|
|
'slot_start' => $slotStart,
|
|
'slot_end' => $slotEnd,
|
|
'status' => $appointment->getStatus(),
|
|
], 201);
|
|
}
|
|
|
|
#[Route('/api/v1/my/appointments', methods: ['GET'])]
|
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
|
public function myAppointments(Request $request, #[CurrentUser] User $user): JsonResponse
|
|
{
|
|
$page = max(1, (int) $request->query->get('page', 1));
|
|
$limit = min(500, max(1, (int) $request->query->get('limit', 15)));
|
|
$search = trim((string) $request->query->get('search', ''));
|
|
$status = trim((string) $request->query->get('status', ''));
|
|
$date = trim((string) $request->query->get('date', ''));
|
|
$doctorUuid = trim((string) $request->query->get('doctor_uuid', ''));
|
|
|
|
$qb = $this->em->createQueryBuilder()
|
|
->select(
|
|
'DISTINCT a.uuid, a.slotStart, a.slotEnd, a.status, a.createdAt, a.version',
|
|
'd.uuid as doctor_uuid, d.name as doctor_name',
|
|
'u.mobileNumber as patient_mobile, u.realName as patient_name'
|
|
)
|
|
->from(Appointment::class, 'a')
|
|
->join('a.doctor', 'd')
|
|
->join('a.user', 'u')
|
|
->orderBy('a.slotStart', 'ASC');
|
|
|
|
$roles = $user->getRoles();
|
|
|
|
if (in_array('ROLE_ADMIN', $roles, true)) {
|
|
// Admin sees all
|
|
} elseif (in_array('ROLE_CLINIC', $roles, true)) {
|
|
$clinic = $this->clinicRepo->findByUser($user);
|
|
if ($clinic === null) {
|
|
return $this->paginated([], 0, $page, $limit);
|
|
}
|
|
$qb->join('App\Clinic\Entity\Clinic', 'c', 'WITH', 'd MEMBER OF c.doctors')
|
|
->andWhere('c = :clinic')
|
|
->setParameter('clinic', $clinic);
|
|
} elseif (in_array('ROLE_DOCTOR', $roles, true)) {
|
|
$doctor = $this->doctorRepo->findByUser($user);
|
|
if ($doctor === null) {
|
|
return $this->paginated([], 0, $page, $limit);
|
|
}
|
|
$qb->andWhere('a.doctor = :doctor')
|
|
->setParameter('doctor', $doctor);
|
|
} elseif (in_array('ROLE_SECRETARY', $roles, true)) {
|
|
$rel = $this->secretaryRepo->findActiveBySecretary($user);
|
|
if ($rel === null) {
|
|
return $this->paginated([], 0, $page, $limit);
|
|
}
|
|
$canView = (bool) ($rel->getPermissions()['resources']['appointments']['view'] ?? false);
|
|
if (!$canView) {
|
|
return $this->paginated([], 0, $page, $limit);
|
|
}
|
|
$qb->andWhere('a.doctor = :doctor')
|
|
->setParameter('doctor', $rel->getDoctor());
|
|
} else {
|
|
return $this->paginated([], 0, $page, $limit);
|
|
}
|
|
|
|
if ($search !== '') {
|
|
$qb->andWhere('u.mobileNumber LIKE :s OR d.name LIKE :s OR u.realName LIKE :s')
|
|
->setParameter('s', '%' . $search . '%');
|
|
}
|
|
if ($status !== '') {
|
|
$qb->andWhere('a.status = :status')->setParameter('status', $status);
|
|
}
|
|
if ($date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
|
$dayStart = (int) strtotime($date . ' 00:00:00');
|
|
$dayEnd = (int) strtotime($date . ' 23:59:59');
|
|
$qb->andWhere('a.slotStart >= :dayStart AND a.slotStart <= :dayEnd')
|
|
->setParameter('dayStart', $dayStart)
|
|
->setParameter('dayEnd', $dayEnd);
|
|
}
|
|
if ($doctorUuid !== '') {
|
|
$qb->andWhere('d.uuid = :doctorUuid')->setParameter('doctorUuid', $doctorUuid);
|
|
}
|
|
|
|
$total = (clone $qb)->select('COUNT(a.id)')->getQuery()->getSingleScalarResult();
|
|
|
|
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
|
|
->getQuery()->getArrayResult();
|
|
|
|
$items = array_map(fn(array $a) => [
|
|
'uuid' => $a['uuid'],
|
|
'patient_name' => $a['patient_name'] ?? '',
|
|
'patient_mobile' => $a['patient_mobile'],
|
|
'doctor_uuid' => $a['doctor_uuid'],
|
|
'doctor_name' => $a['doctor_name'],
|
|
'slot_start' => (int) $a['slotStart'],
|
|
'slot_end' => (int) $a['slotEnd'],
|
|
'appointment_date' => date('Y-m-d', (int) $a['slotStart']),
|
|
'appointment_time' => date('H:i', (int) $a['slotStart']),
|
|
'end_time' => date('H:i', (int) $a['slotEnd']),
|
|
'status' => $a['status'],
|
|
'version' => (int) $a['version'],
|
|
'created_at' => date('c', (int) $a['createdAt']),
|
|
], $rows);
|
|
|
|
return $this->paginated($items, (int) $total, $page, $limit);
|
|
}
|
|
|
|
#[Route('/api/v1/my/appointments/today-stats', methods: ['GET'])]
|
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
|
public function todayStats(Request $request, #[CurrentUser] User $user): JsonResponse
|
|
{
|
|
$date = trim((string) $request->query->get('date', date('Y-m-d')));
|
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
|
$date = date('Y-m-d');
|
|
}
|
|
$dayStart = (int) strtotime($date . ' 00:00:00');
|
|
$dayEnd = (int) strtotime($date . ' 23:59:59');
|
|
|
|
$qb = $this->em->createQueryBuilder()
|
|
->select('a.status, COUNT(a.id) AS cnt')
|
|
->from(Appointment::class, 'a')
|
|
->join('a.doctor', 'd')
|
|
->where('a.slotStart >= :dayStart AND a.slotStart <= :dayEnd')
|
|
->setParameter('dayStart', $dayStart)
|
|
->setParameter('dayEnd', $dayEnd)
|
|
->groupBy('a.status');
|
|
|
|
$roles = $user->getRoles();
|
|
if (in_array('ROLE_CLINIC', $roles, true)) {
|
|
$clinic = $this->clinicRepo->findByUser($user);
|
|
if ($clinic) {
|
|
$qb->join('App\Clinic\Entity\Clinic', 'c', 'WITH', 'd MEMBER OF c.doctors')
|
|
->andWhere('c = :clinic')
|
|
->setParameter('clinic', $clinic);
|
|
}
|
|
} elseif (in_array('ROLE_DOCTOR', $roles, true)) {
|
|
$doctor = $this->doctorRepo->findByUser($user);
|
|
if ($doctor) {
|
|
$qb->andWhere('a.doctor = :doctor')->setParameter('doctor', $doctor);
|
|
}
|
|
} elseif (in_array('ROLE_SECRETARY', $roles, true)) {
|
|
$rel = $this->secretaryRepo->findActiveBySecretary($user);
|
|
if ($rel) {
|
|
$qb->andWhere('a.doctor = :doctor')->setParameter('doctor', $rel->getDoctor());
|
|
}
|
|
}
|
|
|
|
$rows = $qb->getQuery()->getArrayResult();
|
|
$byStatus = [];
|
|
foreach ($rows as $row) {
|
|
$byStatus[$row['status']] = (int) $row['cnt'];
|
|
}
|
|
|
|
$total = array_sum($byStatus);
|
|
$completed = ($byStatus['completed'] ?? 0);
|
|
$cancelled = ($byStatus['cancelled_by_doctor'] ?? 0)
|
|
+ ($byStatus['cancelled_by_user'] ?? 0)
|
|
+ ($byStatus['no_show'] ?? 0)
|
|
+ ($byStatus['expired'] ?? 0);
|
|
$waiting = $total - $completed - $cancelled;
|
|
|
|
return $this->success([
|
|
'total' => $total,
|
|
'completed' => $completed,
|
|
'waiting' => max(0, $waiting),
|
|
'cancelled' => $cancelled,
|
|
]);
|
|
}
|
|
}
|