feat: add staff role functionality with dashboard access and service management

- Implemented SidebarStaff component tests to ensure staff users see only their dashboard and services.
- Created StaffMyServicesPage to display assigned services for staff users.
- Added migration to link clinic staff rows to user accounts for ROLE_STAFF access.
- Defined StaffPermissions class for static permissions related to staff role.
- Introduced StaffRouteGuardSubscriber to restrict API access for staff users.
- Developed StaffAccountService for managing staff user accounts and linking them to clinic staff.
- Added comprehensive tests for StaffAccountService to validate user creation, mobile number handling, and account attachment.
- Implemented tests for staff dashboard access to ensure proper permissions and access control.
- Created tests for staff login context to verify correct environment visibility based on user roles.
This commit is contained in:
hamed
2026-07-30 10:18:41 +03:30
parent 6ec011e3ad
commit 57aeb40934
28 changed files with 1960 additions and 29 deletions
+5 -1
View File
@@ -48,7 +48,9 @@ class ErrorCodes
public const ERR_SECRETARY_001 = 'ERR_SECRETARY_001';
// Staff
public const ERR_STAFF_NOT_FOUND = 'ERR_STAFF_NOT_FOUND';
public const ERR_STAFF_NOT_FOUND = 'ERR_STAFF_NOT_FOUND';
public const ERR_STAFF_MOBILE_INVALID = 'ERR_STAFF_MOBILE_INVALID';
public const ERR_STAFF_MOBILE_TAKEN = 'ERR_STAFF_MOBILE_TAKEN';
// Subscription
public const ERR_SUBSCRIPTION_REQUIRED = 'ERR_SUBSCRIPTION_REQUIRED';
@@ -141,6 +143,8 @@ class ErrorCodes
self::ERR_RATE_LIMIT_001 => 'درخواست‌های زیاد. لطفاً بعداً تلاش کنید',
self::ERR_CAPTCHA_001 => 'تأیید امنیتی ناموفق بود. لطفاً صفحه را رفرش کنید و دوباره تلاش کنید',
self::ERR_STAFF_NOT_FOUND => 'پرسنل یافت نشد',
self::ERR_STAFF_MOBILE_INVALID => 'شماره موبایل پرسنل معتبر نیست',
self::ERR_STAFF_MOBILE_TAKEN => 'برای این شماره قبلاً پرسنلی ثبت شده است',
self::ERR_SUBSCRIPTION_REQUIRED => 'این قابلیت نیاز به پنل Basic یا بالاتر دارد',
self::ERR_TRIAL_ALREADY_USED => 'قبلاً از تریال استفاده کرده‌اید',
self::ERR_TRIAL_DISABLED => 'تریال در حال حاضر غیرفعال است',
+20 -4
View File
@@ -11,6 +11,7 @@ use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Staff\Repository\ClinicStaffRepository;
/**
* تنها نقطهٔ تصمیم‌گیری دربارهٔ «این درخواست در کدام محیط اجرا می‌شود؟».
@@ -34,6 +35,7 @@ class EntityContextResolver
private readonly ClinicRepository $clinicRepo,
private readonly UserActiveContextRepository $activeContextRepo,
private readonly DoctorSecretaryRepository $secretaryRepo,
private readonly ClinicStaffRepository $staffRepo,
) {}
/**
@@ -92,7 +94,13 @@ class EntityContextResolver
return $clinic !== null ? EntityContext::forClinic($clinic) : EntityContext::unknown();
}
/** مالک کلینیک، ادمین، پزشکِ عضو همان کلینیک، یا منشیِ دارای رابطهٔ فعال در آن. */
/**
* مالک کلینیک، ادمین، پزشکِ عضو همان کلینیک، منشیِ دارای رابطهٔ فعال در آن، یا
* پرسنلِ فعالِ همان کلینیک.
*
* «می‌تواند در این محیط بایستد» یعنی محیطش حل می‌شود — نه اینکه هر کاری در آن
* مجاز است؛ محدودهٔ پرسنل را StaffRouteGuardSubscriber تعیین می‌کند.
*/
public function canActInClinic(User $user, Clinic $clinic): bool
{
if ($user->hasRole('ROLE_ADMIN') || $clinic->getUser()->getId() === $user->getId()) {
@@ -104,6 +112,10 @@ class EntityContextResolver
return true;
}
if ($this->staffRepo->findActiveByUserAndEntity($user, EntityContext::TYPE_CLINIC, $clinic->getId()) !== null) {
return true;
}
return $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic) !== null;
}
@@ -148,6 +160,10 @@ class EntityContextResolver
return true;
}
if ($this->staffRepo->findActiveByUserAndEntity($user, EntityContext::TYPE_DOCTOR, $doctor->getId()) !== null) {
return true;
}
return $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor) !== null;
}
@@ -163,9 +179,9 @@ class EntityContextResolver
return $clinic !== null ? EntityContext::forClinic($clinic) : EntityContext::unknown();
}
// منشی fallback نقشی ندارد: محیطش فقط از UserActiveContext می‌آید، چون یک
// منشی می‌تواند هم‌زمان به چند پزشک و کلینیک وصل باشد و نقش تنها، انتخاب
// بین آن‌ها را تعیین نمی‌کند.
// منشی و پرسنل fallback نقشی ندارند: محیطشان فقط از UserActiveContext می‌آید،
// چون هر دو می‌توانند هم‌زمان به چند پزشک و کلینیک وصل باشند و نقش تنها،
// انتخاب بین آن‌ها را تعیین نمی‌کند.
return EntityContext::unknown();
}
}