[]]], responses: [ new OA\Response(response: 200, description: 'پروفایل نماینده'), new OA\Response(response: 404, description: 'کاربر جاری نماینده نیست'), ] )] #[Route('/api/v1/representation/me', methods: ['GET'])] public function me(#[CurrentUser] User $user): JsonResponse { $rep = $this->representationRepo->findByUser($user); if ($rep === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده‌ای برای این کاربر یافت نشد', 404); } return $this->success(['data' => $rep->toArray()]); } #[OA\Post( path: '/api/v1/representation/doctor', summary: 'افزودن پزشک توسط نماینده', security: [['bearerAuth' => []]], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['mobile', 'name'], properties: [ new OA\Property(property: 'mobile', type: 'string'), new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'gender', type: 'string', nullable: true), new OA\Property(property: 'degree', type: 'string', nullable: true), new OA\Property(property: 'medical_system_code', type: 'string', nullable: true), new OA\Property(property: 'specialties', type: 'array', items: new OA\Items(type: 'integer'), nullable: true), ] ) ), responses: [ new OA\Response(response: 201, description: 'پزشک ساخته شد'), new OA\Response(response: 409, description: 'این کاربر قبلاً پزشک است'), new OA\Response(response: 422, description: 'فیلد الزامی وارد نشده'), ] )] #[Route('/api/v1/representation/doctor', methods: ['POST'])] public function createDoctor(Request $request, #[CurrentUser] User $user): JsonResponse { $data = json_decode($request->getContent(), true) ?? []; $mobile = trim((string) ($data['mobile'] ?? '')); $name = trim((string) ($data['name'] ?? '')); if ($mobile === '' || $name === '') { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'موبایل و نام الزامی هستند', 422); } $doctorUser = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]); if (!$doctorUser) { $doctorUser = new User($mobile); $doctorUser->setRealName($name); $doctorUser->setPasswordHash(password_hash(bin2hex(random_bytes(8)), PASSWORD_BCRYPT)); $this->em->persist($doctorUser); } if ($this->em->getRepository(Doctor::class)->findOneBy(['user' => $doctorUser]) !== null) { return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این کاربر قبلاً پروفایل پزشک دارد', 409); } $doctor = new Doctor($doctorUser, $name); if (!empty($data['gender'])) $doctor->setGender($data['gender']); if (!empty($data['degree'])) $doctor->setDegree($data['degree']); if (!empty($data['medical_system_code'])) $doctor->setMedicalSystemCode($data['medical_system_code']); if (!empty($data['info'])) $doctor->setInfo($data['info']); if (!empty($data['mobile_number'])) $doctor->setMobileNumber($data['mobile_number']); if (!empty($data['specialties']) && is_array($data['specialties'])) { foreach ($data['specialties'] as $id) { $s = $this->em->getRepository(Specialty::class)->find((int) $id); if ($s !== null) $doctor->getSpecialties()->add($s); } } $rep = $this->representationRepo->findByUser($user); if ($rep !== null) { $doctor->setRepresentationId($rep->getId()); } $roles = $doctorUser->getRoles(); if (!in_array('ROLE_DOCTOR', $roles, true)) { $roles[] = 'ROLE_DOCTOR'; $doctorUser->setRoles(array_values(array_unique($roles))); } $this->em->persist($doctor); $this->em->flush(); return $this->success(['uuid' => $doctor->getUuid()], 201); } #[OA\Post( path: '/api/v1/representation/clinic', summary: 'افزودن کلینیک توسط نماینده', security: [['bearerAuth' => []]], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['owner_mobile', 'name'], properties: [ new OA\Property(property: 'owner_mobile', type: 'string'), new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'telephone', type: 'string', nullable: true), new OA\Property(property: 'address', type: 'string', nullable: true), ] ) ), responses: [ new OA\Response(response: 200, description: 'کلینیک ساخته شد'), new OA\Response(response: 422, description: 'فیلد الزامی وارد نشده'), ] )] #[Route('/api/v1/representation/clinic', methods: ['POST'])] public function createClinic(Request $request, #[CurrentUser] User $user): JsonResponse { $data = json_decode($request->getContent(), true) ?? []; $mobile = trim((string) ($data['owner_mobile'] ?? '')); $name = trim((string) ($data['name'] ?? '')); if ($mobile === '') { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'شماره موبایل الزامی است', 422); } if ($name === '') { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نام کلینیک الزامی است', 422); } $ownerUser = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]); if (!$ownerUser) { $ownerUser = new User($mobile); $this->em->persist($ownerUser); } $roles = $ownerUser->getRoles(); if (!in_array('ROLE_CLINIC', $roles, true)) { $roles[] = 'ROLE_CLINIC'; $ownerUser->setRoles(array_values(array_unique($roles))); } $clinic = new Clinic($ownerUser); $clinic->setName($name); if (!empty($data['telephone'])) $clinic->setTelephone($data['telephone']); if (!empty($data['address'])) $clinic->setAddress($data['address']); if (!empty($data['info'])) $clinic->setInfo($data['info']); $rep = $this->representationRepo->findByUser($user); if ($rep !== null) { $clinic->setRepresentationId($rep->getId()); } $this->em->persist($clinic); $this->em->flush(); return $this->success([ 'uuid' => $clinic->getUuid(), 'name' => $clinic->getName(), 'is_active' => $clinic->isActive(), ]); } #[OA\Get( path: '/api/v1/representation/doctors', summary: 'پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (paginated، شکلِ یکسان با /admin/doctors)', security: [['bearerAuth' => []]], parameters: [ new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)), new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)), new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')), ], responses: [new OA\Response(response: 200, description: 'لیست پزشکان نماینده')] )] #[Route('/api/v1/representation/doctors', methods: ['GET'])] public function doctors(Request $request, #[CurrentUser] User $user): JsonResponse { $rep = $this->representationRepo->findByUser($user); if ($rep === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده‌ای برای این کاربر یافت نشد', 404); } $page = max(1, (int) $request->query->get('page', 1)); $limit = min(100, max(1, (int) $request->query->get('limit', 15))); $search = trim((string) $request->query->get('search', '')); $qb = $this->em->createQueryBuilder() ->select('d.uuid, d.id, d.name, d.gender, d.degree, d.medicalSystemCode, d.mobileNumber, d.images, d.doctorRate, d.activeDoctorAppointment, d.createdAt') ->from(Doctor::class, 'd') ->where('d.representationId = :repId') ->setParameter('repId', $rep->getId()) ->orderBy('d.createdAt', 'DESC'); if ($search !== '') { $qb->andWhere('d.name LIKE :s OR d.mobileNumber LIKE :s')->setParameter('s', '%' . $search . '%'); } $total = (clone $qb)->select('COUNT(d.id)')->getQuery()->getSingleScalarResult(); $rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit) ->getQuery()->getArrayResult(); $items = array_map(fn(array $d) => [ 'uuid' => $d['uuid'], 'id' => (int) $d['id'], 'name' => $d['name'], 'gender' => $d['gender'], 'degree' => $d['degree'], 'medical_code' => $d['medicalSystemCode'], 'mobile' => $d['mobileNumber'], 'email' => null, 'is_active' => (bool) $d['activeDoctorAppointment'], 'rate' => (float) $d['doctorRate'], 'specialties' => [], 'profile_image' => !empty($d['images']) ? ($d['images'][0]['url'] ?? null) : null, 'created_at' => date('c', (int) $d['createdAt']), ], $rows); return $this->paginated($items, (int) $total, $page, $limit); } #[OA\Get( path: '/api/v1/representation/clinics', summary: 'کلینیک‌های ثبت‌شده توسط نماینده‌ی جاری (paginated، شکلِ یکسان با /admin/clinics)', security: [['bearerAuth' => []]], parameters: [ new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)), new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)), new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')), ], responses: [new OA\Response(response: 200, description: 'لیست کلینیک‌های نماینده')] )] #[Route('/api/v1/representation/clinics', methods: ['GET'])] public function clinics(Request $request, #[CurrentUser] User $user): JsonResponse { $rep = $this->representationRepo->findByUser($user); if ($rep === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده‌ای برای این کاربر یافت نشد', 404); } $page = max(1, (int) $request->query->get('page', 1)); $limit = min(100, max(1, (int) $request->query->get('limit', 15))); $search = trim((string) $request->query->get('search', '')); $qb = $this->em->createQueryBuilder() ->select('c.uuid, c.id, c.name, c.telephone, c.clinicLogo, c.isActive, c.createdAt') ->from(Clinic::class, 'c') ->where('c.representationId = :repId') ->setParameter('repId', $rep->getId()) ->orderBy('c.createdAt', 'DESC'); if ($search !== '') { $qb->andWhere('c.name LIKE :s OR c.telephone LIKE :s')->setParameter('s', '%' . $search . '%'); } $total = (clone $qb)->select('COUNT(c.id)')->getQuery()->getSingleScalarResult(); $rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit) ->getQuery()->getArrayResult(); $items = array_map(fn(array $c) => [ 'uuid' => $c['uuid'], 'id' => (int) $c['id'], 'name' => $c['name'], 'telephone' => $c['telephone'], 'logo' => $c['clinicLogo'], 'clinic_logo' => $c['clinicLogo'], 'is_active' => (bool) $c['isActive'], 'doctors_count'=> 0, 'created_at' => date('c', (int) $c['createdAt']), ], $rows); return $this->paginated($items, (int) $total, $page, $limit); } #[OA\Get( path: '/api/v1/representation/appointments', summary: 'نوبت‌های پزشکانِ زیرمجموعه‌ی نماینده‌ی جاری (paginated)', security: [['bearerAuth' => []]], parameters: [ new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)), new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)), new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'date', in: 'query', required: false, schema: new OA\Schema(type: 'string', format: 'date')), new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')), ], responses: [new OA\Response(response: 200, description: 'لیست نوبت‌ها')] )] #[Route('/api/v1/representation/appointments', methods: ['GET'])] public function appointments(Request $request, #[CurrentUser] User $user): JsonResponse { $rep = $this->representationRepo->findByUser($user); if ($rep === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده‌ای برای این کاربر یافت نشد', 404); } $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', '')); $qb = $this->em->createQueryBuilder() ->select( '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') ->where('d.representationId = :repId') ->setParameter('repId', $rep->getId()) ->orderBy('a.slotStart', 'ASC'); 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); } $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'], 'created_at' => (int) $a['createdAt'], ], $rows); return $this->paginated($items, (int) $total, $page, $limit); } }