feat(secretary): implement scope separation for secretaries in clinics and personal practices

- Added `owner_type` and `clinic_id` fields to `DoctorSecretary` entity to distinguish between clinic and personal practice relationships.
- Updated repository methods to be scope-aware, allowing for specific queries based on the context of the secretary's relationship (clinic or doctor).
- Modified `SecretaryController` to handle secretary creation with appropriate scope based on the current user's role.
- Enhanced `AuthController` to build contexts that reflect the scope of the secretary's access.
- Updated `DashboardController` and `PatientController` to respect the new scope logic when retrieving data.
- Created migration to update the database schema accordingly, dropping the old unique constraint and adding the new fields and constraints.
This commit is contained in:
hamed
2026-06-15 11:19:37 +03:30
parent 5cdcec23a9
commit 4bf381ac94
10 changed files with 727 additions and 95 deletions
@@ -4,8 +4,10 @@ namespace App\Appointment\Controller;
use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Entity\DoctorSecretary;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Controller\BaseController;
use Doctrine\ORM\EntityManagerInterface;
@@ -18,10 +20,11 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class MyAppointmentsController extends BaseController
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly EntityManagerInterface $em,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly UserActiveContextRepository $contextRepo,
) {}
#[Route('/api/v1/my/appointment', methods: ['POST'])]
@@ -127,16 +130,22 @@ class MyAppointmentsController extends BaseController
$qb->andWhere('a.doctor = :doctor')
->setParameter('doctor', $doctor);
} elseif (in_array('ROLE_SECRETARY', $roles, true)) {
$rel = $this->secretaryRepo->findActiveBySecretary($user);
if ($rel === null) {
$filter = $this->resolveSecretaryFilter($user);
if ($filter === null) {
return $this->paginated([], 0, $page, $limit);
}
$canView = (bool) ($rel->getPermissions()['resources']['appointments']['view'] ?? false);
[$filterType, $filterValue, $canView] = $filter;
if (!$canView) {
return $this->paginated([], 0, $page, $limit);
}
$qb->andWhere('a.doctor = :doctor')
->setParameter('doctor', $rel->getDoctor());
if ($filterType === 'clinic') {
$qb->join('App\Clinic\Entity\Clinic', 'c', 'WITH', 'd MEMBER OF c.doctors')
->andWhere('c = :clinic')
->setParameter('clinic', $filterValue);
} else {
$qb->andWhere('a.doctor = :doctor')
->setParameter('doctor', $filterValue);
}
} else {
return $this->paginated([], 0, $page, $limit);
}
@@ -217,9 +226,16 @@ class MyAppointmentsController extends BaseController
$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());
$filter = $this->resolveSecretaryFilter($user);
if ($filter !== null) {
[$filterType, $filterValue] = $filter;
if ($filterType === 'clinic') {
$qb->join('App\Clinic\Entity\Clinic', 'c', 'WITH', 'd MEMBER OF c.doctors')
->andWhere('c = :clinic')
->setParameter('clinic', $filterValue);
} else {
$qb->andWhere('a.doctor = :doctor')->setParameter('doctor', $filterValue);
}
}
}
@@ -244,4 +260,40 @@ class MyAppointmentsController extends BaseController
'cancelled' => $cancelled,
]);
}
/**
* تعیین فیلتر نوبت‌ها برای منشی بر اساس scope فعال:
* Returns [type, entity, canView] یا null اگر رابطه‌ای پیدا نشد.
* type: 'clinic' | 'doctor'
* entity: Clinic | Doctor
*/
private function resolveSecretaryFilter(User $user): ?array
{
$activeCtx = $this->contextRepo->findByUser($user);
$dbUuid = $activeCtx?->getDbUuid();
if ($dbUuid === null) {
return null;
}
// بررسی scope کلینیک
$clinic = $this->clinicRepo->findByUuid($dbUuid);
if ($clinic !== null) {
$rel = $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
if ($rel === null) return null;
$canView = (bool) ($rel->getPermissions()['resources']['appointments']['view'] ?? false);
return ['clinic', $clinic, $canView];
}
// بررسی scope مطب شخصی
$doctor = $this->doctorRepo->findByUuid($dbUuid);
if ($doctor !== null) {
$rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
if ($rel === null) return null;
$canView = (bool) ($rel->getPermissions()['resources']['appointments']['view'] ?? false);
return ['doctor', $doctor, $canView];
}
return null;
}
}
+26 -8
View File
@@ -649,15 +649,33 @@ class AuthController extends BaseController
}
}
// منشی: همه روابط فعال
// منشی: هر رابطه فعال با scope مجزا
foreach ($this->secretaryRepo->findAllActiveBySecretary($user) as $rel) {
$contexts[] = [
'type' => 'doctor',
'db_uuid' => $rel->getDoctor()->getUuid(),
'name' => 'مطب ' . $rel->getDoctor()->getName(),
'role' => 'secretary',
'permissions' => $rel->getPermissions(),
];
if ($rel->getOwnerType() === \App\Secretary\Entity\DoctorSecretary::OWNER_CLINIC && $rel->getClinic() !== null) {
// scope کلینیک — یک context به ازای هر کلینیک (نه هر دکتر)
$clinicUuid = $rel->getClinic()->getUuid();
$alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinicUuid && ($c['role'] ?? '') === 'secretary');
if (empty($alreadyAdded)) {
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinicUuid,
'name' => 'کلینیک ' . ($rel->getClinic()->getName() ?? ''),
'role' => 'secretary',
'scope' => 'clinic',
'permissions' => $rel->getPermissions(),
];
}
} else {
// scope مطب شخصی
$contexts[] = [
'type' => 'doctor',
'db_uuid' => $rel->getDoctor()->getUuid(),
'name' => 'مطب ' . $rel->getDoctor()->getName(),
'role' => 'secretary',
'scope' => 'doctor',
'permissions' => $rel->getPermissions(),
];
}
}
return $contexts;
@@ -3,10 +3,12 @@
namespace App\Dashboard\Controller;
use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Secretary\Entity\DoctorSecretary;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
@@ -21,13 +23,14 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class DashboardController extends BaseController
{
public function __construct(
private readonly ClinicRepository $clinicRepo,
private readonly DoctorRepository $doctorRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly EntityManagerInterface $em,
private readonly SmsWalletService $smsWalletService,
private readonly PatientRecordRepository $patientRecordRepo,
private readonly PatientSessionRepository $patientSessionRepo,
private readonly ClinicRepository $clinicRepo,
private readonly DoctorRepository $doctorRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly EntityManagerInterface $em,
private readonly SmsWalletService $smsWalletService,
private readonly PatientRecordRepository $patientRecordRepo,
private readonly PatientSessionRepository $patientSessionRepo,
private readonly UserActiveContextRepository $contextRepo,
) {}
// ── Clinic Dashboard ────────────────────────────────────────────────────
@@ -250,11 +253,33 @@ class DashboardController extends BaseController
#[IsGranted('ROLE_SECRETARY')]
public function secretary(#[CurrentUser] User $user): JsonResponse
{
$rel = $this->secretaryRepo->findActiveBySecretary($user);
if ($rel === null) {
$activeCtx = $this->contextRepo->findByUser($user);
$dbUuid = $activeCtx?->getDbUuid();
if ($dbUuid === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی منشی تنظیم نشده', 403);
}
// تعیین scope بر اساس db_uuid فعال
$clinic = $this->clinicRepo->findByUuid($dbUuid);
if ($clinic !== null) {
return $this->secretaryClinicDashboard($user, $clinic);
}
$doctor = $this->doctorRepo->findByUuid($dbUuid);
if ($doctor !== null) {
$rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
if ($rel === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی منشی تنظیم نشده', 403);
}
return $this->secretaryDoctorDashboard($user, $rel);
}
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'context نامعتبر است', 403);
}
private function secretaryDoctorDashboard(User $user, DoctorSecretary $rel): JsonResponse
{
$doctor = $rel->getDoctor();
$permissions = $rel->getPermissions();
$canView = (bool) ($permissions['resources']['appointments']['view'] ?? false);
@@ -293,6 +318,7 @@ class DashboardController extends BaseController
}
return $this->success([
'scope' => 'doctor',
'doctor' => [
'uuid' => $doctor->getUuid(),
'name' => $doctor->getName(),
@@ -306,4 +332,71 @@ class DashboardController extends BaseController
'today_appointments' => $todayAppts,
]);
}
private function secretaryClinicDashboard(User $user, \App\Clinic\Entity\Clinic $clinic): JsonResponse
{
$rel = $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
if ($rel === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی منشی تنظیم نشده', 403);
}
$permissions = $rel->getPermissions();
$canView = (bool) ($permissions['resources']['appointments']['view'] ?? false);
$todayStart = strtotime('today midnight');
$todayEnd = strtotime('tomorrow midnight') - 1;
$tmrStart = strtotime('tomorrow midnight');
$tmrEnd = strtotime('tomorrow midnight') + 86399;
$doctors = $this->secretaryRepo->findDoctorsBySecretaryInClinic($user, $clinic);
$todayCount = 0;
$tmrCount = 0;
$todayAppts = [];
if (!empty($doctors)) {
$todayCount = (int) $this->em->createQuery('
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
WHERE a.doctor IN (:doctors) AND a.slotStart >= :s AND a.slotStart <= :e
')->setParameters(['doctors' => $doctors, 's' => $todayStart, 'e' => $todayEnd])
->getSingleScalarResult();
$tmrCount = (int) $this->em->createQuery('
SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
WHERE a.doctor IN (:doctors) AND a.slotStart >= :s AND a.slotStart <= :e
')->setParameters(['doctors' => $doctors, 's' => $tmrStart, 'e' => $tmrEnd])
->getSingleScalarResult();
if ($canView) {
$todayAppts = $this->em->createQuery('
SELECT a.uuid, u.realName AS patient_name, u.mobileNumber AS patient_mobile,
a.slotStart AS slot_start, a.status, d.name AS doctor_name
FROM App\Appointment\Entity\Appointment a
JOIN a.user u
JOIN a.doctor d
WHERE a.doctor IN (:doctors) AND a.slotStart >= :s AND a.slotStart <= :e
ORDER BY a.slotStart ASC
')->setMaxResults(20)->setParameters([
'doctors' => $doctors,
's' => $todayStart,
'e' => $todayEnd,
])->getArrayResult();
}
}
return $this->success([
'scope' => 'clinic',
'clinic' => [
'uuid' => $clinic->getUuid(),
'name' => $clinic->getName(),
],
'permissions' => $permissions,
'stats' => [
'today_appointments' => $todayCount,
'tomorrow_appointments' => $tmrCount,
],
'today_appointments' => $todayAppts,
]);
}
}
+26 -11
View File
@@ -3,6 +3,7 @@
namespace App\Patient\Controller;
use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Auth\Repository\UserRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
@@ -25,14 +26,15 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class PatientController extends BaseController
{
public function __construct(
private readonly PatientRecordRepository $recordRepo,
private readonly PatientSessionRepository $sessionRepo,
private readonly PatientService $patientService,
private readonly SubscriptionService $subscriptionService,
private readonly UserRepository $userRepo,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly PatientRecordRepository $recordRepo,
private readonly PatientSessionRepository $sessionRepo,
private readonly PatientService $patientService,
private readonly SubscriptionService $subscriptionService,
private readonly UserRepository $userRepo,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly UserActiveContextRepository $contextRepo,
) {}
#[Route('/api/v1/patients', methods: ['GET'])]
@@ -173,9 +175,22 @@ class PatientController extends BaseController
}
if ($user->hasRole('ROLE_SECRETARY')) {
$secretary = $this->secretaryRepo->findActiveBySecretary($user);
if ($secretary !== null && ($secretary->getPermissions()['appointments']['view'] ?? false)) {
return ['doctor', $secretary->getDoctor()->getId()];
$dbUuid = $this->contextRepo->findByUser($user)?->getDbUuid();
if ($dbUuid !== null) {
$clinic = $this->clinicRepo->findByUuid($dbUuid);
if ($clinic !== null) {
$rel = $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
if ($rel !== null) {
return ['clinic', $clinic->getId()];
}
}
$doctor = $this->doctorRepo->findByUuid($dbUuid);
if ($doctor !== null) {
$rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
if ($rel !== null) {
return ['doctor', $doctor->getId()];
}
}
}
}
@@ -4,9 +4,7 @@ namespace App\Secretary\Controller;
use App\Auth\Entity\User;
use App\Auth\Repository\UserRepository;
use App\Clinic\Entity\Clinic;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Entity\Doctor;
use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Entity\DoctorSecretary;
use App\Secretary\Repository\DoctorSecretaryRepository;
@@ -48,14 +46,24 @@ class SecretaryController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
if (!$this->canManageDoctor($doctor, $currentUser)) {
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
// تعیین scope بر اساس نقش کاربر جاری
$ownerClinic = null;
if ($currentUser->hasRole('ROLE_CLINIC')) {
$ownerClinic = $this->clinicRepo->findByUser($currentUser);
if ($ownerClinic === null || !$this->secretaryRepo->isDoctorInClinic($doctor, $ownerClinic)) {
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
}
} elseif (!$currentUser->hasRole('ROLE_ADMIN')) {
// دکتر فقط برای خودش منشی تعریف می‌کند
if ($doctor->getUser()->getId() !== $currentUser->getId()) {
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
}
}
// Check plan limit (dynamic via SubscriptionService)
$entityType = 'doctor';
$entityId = $doctor->getId();
$limit = $this->subscriptionService->getSecretaryLimit($entityType, $entityId);
$ownerType = $ownerClinic !== null ? DoctorSecretary::OWNER_CLINIC : DoctorSecretary::OWNER_DOCTOR;
// Check plan limit
$limit = $this->subscriptionService->getSecretaryLimit('doctor', $doctor->getId());
$activeCount = $this->secretaryRepo->countActiveByDoctor($doctor);
if ($activeCount >= $limit) {
return $this->error(ErrorCodes::ERR_SECRETARY_001, ErrorCodes::message(ErrorCodes::ERR_SECRETARY_001), 422);
@@ -65,7 +73,6 @@ class SecretaryController extends BaseController
$secretaryUser = $this->userRepo->findByMobile($mobile);
if ($secretaryUser === null) {
$secretaryUser = new User($mobile);
// Set a temporary password if provided
if (!empty($data['password'])) {
$hash = $this->hasher->hashPassword($secretaryUser, $data['password']);
$secretaryUser->setPasswordHash($hash);
@@ -80,15 +87,18 @@ class SecretaryController extends BaseController
}
$this->userRepo->save($secretaryUser);
// Check duplicate
$existing = $this->secretaryRepo->findOneBy(['doctor' => $doctor, 'secretary' => $secretaryUser]);
// Check duplicate within same scope
$existing = $this->secretaryRepo->findOneBy([
'doctor' => $doctor,
'secretary' => $secretaryUser,
'ownerType' => $ownerType,
]);
if ($existing !== null) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این منشی قبلاً اضافه شده است', 409);
}
$secretary = new DoctorSecretary($doctor, $secretaryUser);
$secretary = new DoctorSecretary($doctor, $secretaryUser, $ownerType, $ownerClinic);
// Apply custom permissions if provided
if (!empty($data['permissions'])) {
$secretary->mergePermissions($data['permissions']);
}
@@ -157,6 +167,7 @@ class SecretaryController extends BaseController
return $this->success(['message' => 'منشی با موفقیت حذف شد']);
}
/** لیست منشیان مطب شخصی یک دکتر */
#[Route('/api/v1/secretaries/{doctorUuid}', methods: ['GET'])]
public function list(string $doctorUuid, #[CurrentUser] User $currentUser): JsonResponse
{
@@ -165,18 +176,20 @@ class SecretaryController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
if (!$this->canManageDoctor($doctor, $currentUser)) {
// فقط خود دکتر یا ادمین می‌توانند منشیان مطب شخصی را ببینند
if (!$currentUser->hasRole('ROLE_ADMIN') && $doctor->getUser()->getId() !== $currentUser->getId()) {
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
}
$secretaries = array_map(
fn(DoctorSecretary $s) => $s->toArray(),
$this->secretaryRepo->findByDoctor($doctor)
$this->secretaryRepo->findByDoctorScope($doctor)
);
return $this->success(['data' => $secretaries]);
return $this->success($secretaries);
}
/** لیست همه منشیان کلینیک (از همه دکترها، owner_type='clinic') */
#[Route('/api/v1/secretaries/clinic/{clinicUuid}', methods: ['GET'])]
public function listByClinic(string $clinicUuid, #[CurrentUser] User $currentUser): JsonResponse
{
@@ -194,30 +207,28 @@ class SecretaryController extends BaseController
$this->secretaryRepo->findByClinic($clinic)
);
return $this->success(['data' => $secretaries]);
return $this->success($secretaries);
}
/** بررسی دسترسی برای ویرایش/حذف یک رابطه منشی — scope-aware */
private function canManage(DoctorSecretary $secretary, User $user): bool
{
return $this->canManageDoctor($secretary->getDoctor(), $user);
}
private function canManageDoctor(Doctor $doctor, User $user): bool
{
if ($user->hasRole('ROLE_ADMIN')) {
return true;
}
if ($doctor->getUser()->getId() === $user->getId()) {
return true;
if ($secretary->getOwnerType() === DoctorSecretary::OWNER_DOCTOR) {
// فقط خود دکتر می‌تواند منشی مطب شخصی‌اش را مدیریت کند
return $secretary->getDoctor()->getUser()->getId() === $user->getId();
}
// clinic owner can manage secretaries of its own doctors
if ($user->hasRole('ROLE_CLINIC')) {
$clinic = $this->clinicRepo->findByUser($user);
if ($clinic !== null && $this->secretaryRepo->isDoctorInClinic($doctor, $clinic)) {
return true;
if ($secretary->getOwnerType() === DoctorSecretary::OWNER_CLINIC) {
// فقط مالک کلینیک می‌تواند منشیان کلینیکی را مدیریت کند
if (!$user->hasRole('ROLE_CLINIC')) {
return false;
}
$clinic = $this->clinicRepo->findByUser($user);
return $clinic !== null && $secretary->getClinic()?->getId() === $clinic->getId();
}
return false;
+27 -9
View File
@@ -3,15 +3,19 @@
namespace App\Secretary\Entity;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity]
#[ORM\Table(name: 'doctor_secretaries')]
#[ORM\UniqueConstraint(name: 'idx_doctor_secretaries_pair', columns: ['doctor_id', 'secretary_id'])]
#[ORM\UniqueConstraint(name: 'idx_doctor_secretary_scope', columns: ['doctor_id', 'secretary_id', 'owner_type'])]
class DoctorSecretary
{
public const OWNER_DOCTOR = 'doctor';
public const OWNER_CLINIC = 'clinic';
public const DEFAULT_PERMISSIONS = [
'version' => 1,
'resources' => [
@@ -38,6 +42,13 @@ class DoctorSecretary
#[ORM\JoinColumn(name: 'secretary_id', referencedColumnName: 'id', nullable: false)]
private User $secretary;
#[ORM\Column(name: 'owner_type', type: 'string', length: 10, options: ['default' => 'doctor'])]
private string $ownerType;
#[ORM\ManyToOne(targetEntity: Clinic::class)]
#[ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Clinic $clinic = null;
#[ORM\Column(name: 'permission', type: 'json', nullable: true)]
private ?array $permissions = null;
@@ -50,25 +61,30 @@ class DoctorSecretary
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
public function __construct(Doctor $doctor, User $secretary)
public function __construct(Doctor $doctor, User $secretary, string $ownerType = self::OWNER_DOCTOR, ?Clinic $clinic = null)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->doctor = $doctor;
$this->secretary = $secretary;
$this->ownerType = $ownerType;
$this->clinic = $clinic;
$this->permissions = self::DEFAULT_PERMISSIONS;
$this->createdAt = time();
$this->updatedAt = time();
}
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getDoctor(): Doctor { return $this->doctor; }
public function getSecretary(): User { return $this->secretary; }
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getDoctor(): Doctor { return $this->doctor; }
public function getSecretary(): User { return $this->secretary; }
public function getOwnerType(): string { return $this->ownerType; }
public function getClinic(): ?Clinic { return $this->clinic; }
public function getPermissions(): array { return $this->permissions ?? self::DEFAULT_PERMISSIONS; }
public function isActive(): bool { return $this->active; }
public function getCreatedAt(): int { return $this->createdAt; }
public function isActive(): bool { return $this->active; }
public function getCreatedAt(): int { return $this->createdAt; }
public function getUpdatedAt(): int { return $this->updatedAt; }
public function setActive(bool $v): self { $this->active = $v; $this->touch(); return $this; }
public function setActive(bool $v): self { $this->active = $v; $this->touch(); return $this; }
public function setPermissions(array $v): self { $this->permissions = $v; $this->touch(); return $this; }
/** Deep merge: only provided resources/actions are updated */
@@ -102,6 +118,8 @@ class DoctorSecretary
'mobile_number' => $this->secretary->getMobileNumber(),
'doctor_name' => $this->doctor->getName(),
'doctor_uuid' => $this->doctor->getUuid(),
'owner_type' => $this->ownerType,
'clinic_uuid' => $this->clinic?->getUuid(),
'is_active' => $this->active,
'permissions' => $this->getPermissions(),
'created_at' => $this->createdAt,
@@ -38,37 +38,87 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
return $this->findBy(['doctor' => $doctor], ['createdAt' => 'DESC']);
}
public function isDoctorInClinic(Doctor $doctor, Clinic $clinic): bool
/** منشیان مطب شخصی یک دکتر (owner_type='doctor') */
public function findByDoctorScope(Doctor $doctor): array
{
return $clinic->getDoctors()->contains($doctor);
return $this->findBy(
['doctor' => $doctor, 'ownerType' => DoctorSecretary::OWNER_DOCTOR],
['createdAt' => 'DESC']
);
}
/** @return DoctorSecretary[] — all secretaries across all doctors of a clinic */
public function findByClinic(Clinic $clinic): array
/** رابطه منشی در scope مطب شخصی */
public function findActiveBySecretaryForDoctor(User $user, Doctor $doctor): ?DoctorSecretary
{
$doctorIds = $clinic->getDoctors()->map(fn(Doctor $d) => $d->getId())->toArray();
if (empty($doctorIds)) {
return [];
}
return $this->findOneBy([
'secretary' => $user,
'doctor' => $doctor,
'ownerType' => DoctorSecretary::OWNER_DOCTOR,
'active' => true,
]);
}
/** اولین رابطه فعال منشی در یک کلینیک (برای چک دسترسی کلی) */
public function findActiveBySecretaryForClinic(User $user, Clinic $clinic): ?DoctorSecretary
{
return $this->createQueryBuilder('s')
->where('s.secretary = :user')
->andWhere('s.clinic = :clinic')
->andWhere('s.ownerType = :type')
->andWhere('s.active = true')
->setParameter('user', $user)
->setParameter('clinic', $clinic)
->setParameter('type', DoctorSecretary::OWNER_CLINIC)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
/** همه دکترهای کلینیک که این منشی به آن‌ها متصل است */
public function findDoctorsBySecretaryInClinic(User $user, Clinic $clinic): array
{
return $this->createQueryBuilder('s')
->select('d')
->join('s.doctor', 'd')
->where('d.id IN (:ids)')
->setParameter('ids', $doctorIds)
->orderBy('s.createdAt', 'DESC')
->where('s.secretary = :user')
->andWhere('s.clinic = :clinic')
->andWhere('s.ownerType = :type')
->andWhere('s.active = true')
->setParameter('user', $user)
->setParameter('clinic', $clinic)
->setParameter('type', DoctorSecretary::OWNER_CLINIC)
->getQuery()
->getResult();
}
/** همه روابط فعال یک منشی — برای auth context builder */
public function findAllActiveBySecretary(User $user): array
{
return $this->findBy(['secretary' => $user, 'active' => true]);
}
/** اولین رابطه فعال — backward compat برای کدهایی که هنوز migrate نشده‌اند */
public function findActiveBySecretary(User $user): ?DoctorSecretary
{
return $this->findOneBy(['secretary' => $user, 'active' => true]);
}
/** @return DoctorSecretary[] */
public function findAllActiveBySecretary(User $user): array
public function isDoctorInClinic(Doctor $doctor, Clinic $clinic): bool
{
return $this->findBy(['secretary' => $user, 'active' => true]);
return $clinic->getDoctors()->contains($doctor);
}
/** همه منشیان کلینیک (owner_type='clinic') */
public function findByClinic(Clinic $clinic): array
{
return $this->createQueryBuilder('s')
->where('s.clinic = :clinic')
->andWhere('s.ownerType = :type')
->setParameter('clinic', $clinic)
->setParameter('type', DoctorSecretary::OWNER_CLINIC)
->orderBy('s.createdAt', 'DESC')
->getQuery()
->getResult();
}
public function save(DoctorSecretary $entity, bool $flush = true): void