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, ]); } }