feat: add clinic doctor invitation feature
- Implemented ClinicInvitationController to handle doctor invitations. - Created ClinicDoctorInvitation entity and repository for managing invitations. - Added ClinicInvitationService for business logic related to invitations. - Introduced endpoints for inviting, listing, resending, changing status, and deleting invitations. - Updated security configuration to allow public access to invitation endpoints. - Added migration for clinic_doctor_invitations table. - Enhanced DoctorRepository with a method to find doctors by mobile number. - Updated ClinicDetailPage to include invitation management UI.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\ClinicInvitation\Controller;
|
||||
|
||||
use App\ClinicInvitation\Repository\ClinicDoctorInvitationRepository;
|
||||
use App\ClinicInvitation\Service\ClinicInvitationService;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Exception\AppException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class ClinicInvitationController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClinicInvitationService $invitationService,
|
||||
private readonly ClinicDoctorInvitationRepository $invRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
) {}
|
||||
|
||||
// ── Admin endpoints ──────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/admin/clinic/{uuid}/invite-doctor', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function inviteDoctor(string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
$clinic = $this->clinicRepo->findByUuid($uuid);
|
||||
if (!$clinic) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'کلینیک یافت نشد', 404);
|
||||
}
|
||||
|
||||
$body = json_decode($request->getContent(), true) ?? [];
|
||||
$mobile = trim($body['mobile'] ?? '');
|
||||
$name = !empty($body['name']) ? trim($body['name']) : null;
|
||||
$specialty = !empty($body['specialty']) ? trim($body['specialty']) : null;
|
||||
|
||||
if (!$mobile || !preg_match('/^09\d{9}$/', $mobile)) {
|
||||
throw new AppException('ERR_VALIDATION_001', 'شماره موبایل نامعتبر است', 422, 'mobile');
|
||||
}
|
||||
|
||||
$inv = $this->invitationService->invite($clinic, $this->getUser(), $mobile, $name, $specialty);
|
||||
|
||||
return $this->success($inv->toArray(), 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/clinic/{uuid}/invitations', methods: ['GET'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function listInvitations(string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
$clinic = $this->clinicRepo->findByUuid($uuid);
|
||||
if (!$clinic) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'کلینیک یافت نشد', 404);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(50, max(1, (int) $request->query->get('limit', 20)));
|
||||
|
||||
$qb = $this->invRepo->createQueryBuilder('i')
|
||||
->leftJoin('i.doctor', 'd')
|
||||
->where('i.clinic = :clinic')
|
||||
->setParameter('clinic', $clinic)
|
||||
->orderBy('i.invitedAt', 'DESC');
|
||||
|
||||
$total = (clone $qb)->select('COUNT(i.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
$items = $qb->setFirstResult(($page - 1) * $limit)
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$data = array_map(fn($inv) => $inv->toArray(), $items);
|
||||
|
||||
return $this->paginated($data, (int) $total, $page, $limit);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/clinic/invitation/{invUuid}/resend', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function resendInvitation(string $invUuid): JsonResponse
|
||||
{
|
||||
$inv = $this->invRepo->findOneBy(['uuid' => $invUuid]);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
}
|
||||
|
||||
$this->invitationService->resend($inv);
|
||||
|
||||
return $this->success(['message' => 'پیامک مجدداً ارسال شد']);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/clinic/invitation/{invUuid}/status', methods: ['PATCH'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function changeInvitationStatus(string $invUuid, Request $request): JsonResponse
|
||||
{
|
||||
$inv = $this->invRepo->findOneBy(['uuid' => $invUuid]);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
}
|
||||
|
||||
$body = json_decode($request->getContent(), true) ?? [];
|
||||
$status = $body['status'] ?? '';
|
||||
|
||||
$this->invitationService->changeStatus($inv, $status);
|
||||
|
||||
return $this->success(['status' => $inv->getStatus()]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/clinic/invitation/{invUuid}', methods: ['DELETE'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function deleteInvitation(string $invUuid): JsonResponse
|
||||
{
|
||||
$inv = $this->invRepo->findOneBy(['uuid' => $invUuid]);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
}
|
||||
|
||||
$this->invitationService->delete($inv);
|
||||
|
||||
return $this->success(null, 204);
|
||||
}
|
||||
|
||||
// ── Public endpoints ──────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/clinic-invitation/{token}', methods: ['GET'])]
|
||||
public function viewInvitation(string $token): JsonResponse
|
||||
{
|
||||
$inv = $this->invRepo->findByToken($token);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
}
|
||||
|
||||
$clinic = $inv->getClinic();
|
||||
|
||||
return $this->success([
|
||||
'invitation' => $inv->toArray(),
|
||||
'clinic' => [
|
||||
'uuid' => $clinic->getUuid(),
|
||||
'name' => $clinic->getName(),
|
||||
'logo' => $clinic->getClinicLogo(),
|
||||
],
|
||||
'is_usable' => $inv->isUsable(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/clinic-invitation/{token}/accept', methods: ['POST'])]
|
||||
public function acceptInvitation(string $token): JsonResponse
|
||||
{
|
||||
$inv = $this->invRepo->findByToken($token);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
}
|
||||
|
||||
$this->invitationService->accept($inv);
|
||||
|
||||
return $this->success(['message' => 'دعوتنامه پذیرفته شد']);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/clinic-invitation/{token}/reject', methods: ['POST'])]
|
||||
public function rejectInvitation(string $token): JsonResponse
|
||||
{
|
||||
$inv = $this->invRepo->findByToken($token);
|
||||
if (!$inv) {
|
||||
throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه یافت نشد', 404);
|
||||
}
|
||||
|
||||
$this->invitationService->reject($inv);
|
||||
|
||||
return $this->success(['message' => 'دعوتنامه رد شد']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user