388 lines
18 KiB
PHP
388 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Representation\Controller;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Auth\Entity\User;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Specialty\Entity\Specialty;
|
|
use App\Representation\Repository\RepresentationRepository;
|
|
use App\Shared\Constant\ErrorCodes;
|
|
use App\Shared\Controller\BaseController;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use OpenApi\Attributes as OA;
|
|
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;
|
|
|
|
/**
|
|
* اکشنهای پنل نماینده: افزودن پزشک/کلینیک، نوبتهای پزشکانِ زیرمجموعه، و پروفایل نمایندهی جاری.
|
|
* هر اکشن برای ROLE_REPRESENTATION (و ROLE_ADMIN) باز است؛ مالکیت همیشه از کاربر جاری تعیین میشود.
|
|
*/
|
|
#[OA\Tag(name: 'Representations')]
|
|
#[IsGranted('ROLE_REPRESENTATION')]
|
|
class RepresentationActionController extends BaseController
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly RepresentationRepository $representationRepo,
|
|
) {}
|
|
|
|
#[OA\Get(
|
|
path: '/api/v1/representation/me',
|
|
summary: 'پروفایل نمایندهی کاربر جاری',
|
|
security: [['bearerAuth' => []]],
|
|
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);
|
|
}
|
|
}
|