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,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\ClinicInvitation\Entity;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\ClinicInvitation\Repository\ClinicDoctorInvitationRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ClinicDoctorInvitationRepository::class)]
|
||||
#[ORM\Table(name: 'clinic_doctor_invitations')]
|
||||
#[ORM\Index(columns: ['token'], name: 'idx_cdi_token')]
|
||||
#[ORM\Index(columns: ['clinic_id'], name: 'idx_cdi_clinic')]
|
||||
#[ORM\Index(columns: ['mobile'], name: 'idx_cdi_mobile')]
|
||||
class ClinicDoctorInvitation
|
||||
{
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_ACCEPTED = 'accepted';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
public const STATUS_SUSPENDED = 'suspended';
|
||||
public const STATUS_REMOVED = 'removed';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Clinic::class)]
|
||||
#[ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private Clinic $clinic;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(name: 'invited_by_id', referencedColumnName: 'id', nullable: false)]
|
||||
private User $invitedBy;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Doctor::class)]
|
||||
#[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?Doctor $doctor = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 20)]
|
||||
private string $mobile;
|
||||
|
||||
#[ORM\Column(name: 'invited_name', type: 'string', length: 255, nullable: true)]
|
||||
private ?string $invitedName = null;
|
||||
|
||||
#[ORM\Column(name: 'invited_specialty', type: 'string', length: 255, nullable: true)]
|
||||
private ?string $invitedSpecialty = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 128, unique: true)]
|
||||
private string $token;
|
||||
|
||||
#[ORM\Column(name: 'token_used', type: 'boolean')]
|
||||
private bool $tokenUsed = false;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 20)]
|
||||
private string $status = self::STATUS_PENDING;
|
||||
|
||||
#[ORM\Column(name: 'invited_at', type: 'integer')]
|
||||
private int $invitedAt;
|
||||
|
||||
#[ORM\Column(name: 'expires_at', type: 'integer')]
|
||||
private int $expiresAt;
|
||||
|
||||
#[ORM\Column(name: 'responded_at', type: 'integer', nullable: true)]
|
||||
private ?int $respondedAt = null;
|
||||
|
||||
public function __construct(Clinic $clinic, User $invitedBy, string $mobile)
|
||||
{
|
||||
$this->uuid = \Symfony\Component\Uid\Uuid::v4()->toRfc4122();
|
||||
$this->clinic = $clinic;
|
||||
$this->invitedBy = $invitedBy;
|
||||
$this->mobile = $mobile;
|
||||
$this->token = bin2hex(random_bytes(48));
|
||||
$this->invitedAt = time();
|
||||
$this->expiresAt = $this->invitedAt + 72 * 3600;
|
||||
}
|
||||
|
||||
public function isUsable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING
|
||||
&& !$this->tokenUsed
|
||||
&& time() < $this->expiresAt;
|
||||
}
|
||||
|
||||
public function markUsed(): void
|
||||
{
|
||||
$this->tokenUsed = true;
|
||||
$this->respondedAt = time();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->token = bin2hex(random_bytes(48));
|
||||
$this->tokenUsed = false;
|
||||
$this->invitedAt = time();
|
||||
$this->expiresAt = $this->invitedAt + 72 * 3600;
|
||||
$this->status = self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'mobile' => $this->mobile,
|
||||
'invited_name' => $this->invitedName,
|
||||
'invited_specialty' => $this->invitedSpecialty,
|
||||
'status' => $this->status,
|
||||
'token_used' => $this->tokenUsed,
|
||||
'invited_at' => $this->invitedAt,
|
||||
'expires_at' => $this->expiresAt,
|
||||
'responded_at' => $this->respondedAt,
|
||||
'doctor' => $this->doctor ? [
|
||||
'uuid' => $this->doctor->getUuid(),
|
||||
'name' => $this->doctor->getName(),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getClinic(): Clinic { return $this->clinic; }
|
||||
public function getDoctor(): ?Doctor { return $this->doctor; }
|
||||
public function getMobile(): string { return $this->mobile; }
|
||||
public function getToken(): string { return $this->token; }
|
||||
public function getStatus(): string { return $this->status; }
|
||||
public function getInvitedAt(): int { return $this->invitedAt; }
|
||||
public function getExpiresAt(): int { return $this->expiresAt; }
|
||||
public function isTokenUsed(): bool { return $this->tokenUsed; }
|
||||
public function getInvitedName(): ?string { return $this->invitedName; }
|
||||
public function getInvitedSpecialty(): ?string { return $this->invitedSpecialty; }
|
||||
|
||||
public function setDoctor(?Doctor $doctor): void { $this->doctor = $doctor; }
|
||||
public function setStatus(string $status): void { $this->status = $status; }
|
||||
public function setInvitedName(?string $n): void { $this->invitedName = $n; }
|
||||
public function setInvitedSpecialty(?string $s): void { $this->invitedSpecialty = $s; }
|
||||
}
|
||||
Reference in New Issue
Block a user