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;
}
}