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
@@ -4,6 +4,8 @@ namespace App\Dashboard\Controller;
use App\Auth\Entity\User;
use App\Clinic\Repository\ClinicRepository;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
@@ -13,6 +15,8 @@ use App\Shared\Constant\ErrorCodes;
use App\Shared\Context\EntityContextResolver;
use App\Shared\Controller\BaseController;
use App\Sms\Service\SmsWalletService;
use App\Staff\Repository\ClinicStaffRepository;
use App\Staff\Security\StaffPermissions;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -35,6 +39,8 @@ class DashboardController extends BaseController
private readonly EntityContextResolver $contextResolver,
private readonly \App\Clinic\Security\ClinicDoctorPermissionChecker $permChecker,
private readonly \App\Doctor\Repository\DoctorAddressRepository $addressRepo,
private readonly ClinicStaffRepository $staffRepo,
private readonly ServiceItemRepository $serviceItemRepo,
) {}
// ── Clinic Dashboard ────────────────────────────────────────────────────
@@ -668,5 +674,75 @@ class DashboardController extends BaseController
'today_appointments' => $todayAppts,
]);
}
// ── Staff Dashboard ──────────────────────────────────────────────────────
/**
* داشبورد پرسنل: سرویس‌هایی که به او تخصیص یافته و نوبت‌های امروزِ خودش.
*
* نقش تنها کافی نیست — ردیف فعالِ پرسنل در محیط فعال هم باید وجود داشته باشد،
* چون توکنِ صادرشده تا انقضا معتبر می‌ماند و غیرفعال‌شدنِ پرسنل باید همان لحظه
* دسترسی را ببندد.
*/
#[Route('/api/v1/dashboard/staff', methods: ['GET'])]
#[IsGranted('ROLE_STAFF')]
public function staff(#[CurrentUser] User $user): JsonResponse
{
$context = $this->contextResolver->resolve($user);
if (!$context->isResolved()) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'محیط کاری پرسنل تنظیم نشده', 403);
}
[$entityType, $entityId] = $context->toEntityPair();
$staff = $this->staffRepo->findActiveByUserAndEntity($user, $entityType, $entityId);
if ($staff === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی پرسنل تنظیم نشده', 403);
}
$todayStart = strtotime('today midnight');
$todayEnd = strtotime('tomorrow midnight') - 1;
$todayAppts = $this->em->createQuery('
SELECT a.uuid, u.realName AS patient_name, u.mobileNumber AS patient_mobile,
a.slotStart AS slot_start, a.status
FROM App\Appointment\Entity\Appointment a
JOIN a.user u
WHERE a.staff = :staff AND a.slotStart >= :s AND a.slotStart <= :e
ORDER BY a.slotStart ASC
')->setParameters(['staff' => $staff, 's' => $todayStart, 'e' => $todayEnd])
->getArrayResult();
$services = array_map(
static fn(ServiceItem $item) => [
'uuid' => $item->getUuid(),
'name' => $item->getName(),
'section_name' => $item->getSection()->getName(),
'price_rials' => $item->getPriceRials(),
'duration_minutes' => $item->getDurationMinutes(),
],
$this->serviceItemRepo->findByStaff($staff),
);
return $this->success([
'scope' => $entityType,
'staff' => [
'uuid' => $staff->getUuid(),
'full_name' => $staff->getFullName(),
'job_title' => $staff->getJobTitle(),
],
'owner' => [
'name' => $context->isClinic()
? ($context->clinic?->getName() ?? '')
: ($context->doctor?->getName() ?? ''),
],
'permissions' => StaffPermissions::DEFAULT,
'stats' => [
'today_appointments' => count($todayAppts),
'services' => count($services),
],
'services' => $services,
'today_appointments' => $todayAppts,
]);
}
}