سرویسهای تخصیصیافته
@@ -1053,12 +1123,16 @@ function StaffDashboard() {
)}
-
-
-
نوبتهای امروز من
+ {/* نوبتِ مستقیمِ اختصاصیافته فقط وقتی نشان داده میشود که وجود داشته باشد؛
+ یک جدول همیشه-خالی زیر کارِ واقعی، فقط صفحه را بلند میکرد. */}
+ {(d.today_appointments?.length ?? 0) > 0 && (
+
+
+
نوبتهای اختصاصیافته به من
+
+
-
-
+ )}
);
}
diff --git a/docs/api/dashboard.md b/docs/api/dashboard.md
index efdab7e3..f1522cdf 100644
--- a/docs/api/dashboard.md
+++ b/docs/api/dashboard.md
@@ -244,7 +244,16 @@ Returns stats for the authenticated secretary and (conditionally) today's appoin
## GET /api/v1/dashboard/staff
-داشبورد پرسنل: سرویسهایی که به این پرسنل تخصیص یافته و نوبتهای امروزِ خودش.
+داشبورد پرسنل: کارِ امروزِ اپراتور و سرویسهایی که مجاز به انجامشان است.
+
+> **کارِ امروز از جلسات درمان میآید، نه از `appointments.staff_id`.** آن ستون را هیچ
+> مسیرِ نوبتدهی بهصورت پیشفرض پر نمیکند، پس `today_appointments` برای اپراتوری که
+> تمام روز جلسه دارد هم خالی است. `today_sessions` همان صفِ
+> `TreatmentSessionRepository::findTodayForStaff` است — یک قاعده، نه دو تا. قواعدش در
+> [treatment.md](treatment.md#جلسات-امروز-من-یک-صف-است).
+>
+> `services` هم اتحادِ دو منبع است: تخصیصِ مستقیمِ سرویس، و پروتکلِ «طول درمان» که این
+> پرسنل را مجاز دانسته. بدون دومی، اپراتورِ لیزر «هیچ سرویسی ندارید» میدید.
**Auth:** `ROLE_STAFF` — و علاوه بر نقش، باید ردیف **فعالِ** `clinic_staff` در محیط فعال وجود
داشته باشد. توکن تا انقضا معتبر میماند، پس غیرفعالکردن پرسنل همان لحظه با همین بررسی
@@ -270,7 +279,7 @@ Returns stats for the authenticated secretary and (conditionally) today's appoin
"version": 1,
"resources": { "services": { "view": true }, "appointments": { "view": true } }
},
- "stats": { "today_appointments": 0, "services": 1 },
+ "stats": { "today_sessions": 2, "today_appointments": 0, "services": 1 },
"services": [
{
"uuid": "f9ffb607-f137-4ffb-8327-76427fd6fe55",
@@ -280,6 +289,17 @@ Returns stats for the authenticated secretary and (conditionally) today's appoin
"duration_minutes": null
}
],
+ "today_sessions": [
+ {
+ "uuid": "86300220-c8e3-4928-b5e2-ce54a75cd59b",
+ "session_number": 1,
+ "total_sessions": 3,
+ "status": "in_progress",
+ "service_name": "لیزر توتال",
+ "patient_name": "محمد رسولی",
+ "slot_start": 1786084200
+ }
+ ],
"today_appointments": []
}
}
diff --git a/src/Dashboard/Controller/DashboardController.php b/src/Dashboard/Controller/DashboardController.php
index f7ad2e1f..28ff0e9c 100644
--- a/src/Dashboard/Controller/DashboardController.php
+++ b/src/Dashboard/Controller/DashboardController.php
@@ -17,6 +17,9 @@ use App\Shared\Controller\BaseController;
use App\Sms\Service\SmsWalletService;
use App\Staff\Repository\ClinicStaffRepository;
use App\Staff\Security\StaffPermissions;
+use App\Treatment\Entity\TreatmentSession;
+use App\Treatment\Repository\TreatmentProtocolRepository;
+use App\Treatment\Repository\TreatmentSessionRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -37,6 +40,8 @@ class DashboardController extends BaseController
private readonly PatientRecordRepository $patientRecordRepo,
private readonly PatientSessionRepository $patientSessionRepo,
private readonly EntityContextResolver $contextResolver,
+ private readonly TreatmentSessionRepository $treatmentSessionRepo,
+ private readonly TreatmentProtocolRepository $protocolRepo,
private readonly \App\Clinic\Security\ClinicDoctorPermissionChecker $permChecker,
private readonly \App\Doctor\Repository\DoctorAddressRepository $addressRepo,
private readonly ClinicStaffRepository $staffRepo,
@@ -726,6 +731,40 @@ class DashboardController extends BaseController
')->setParameters(['staff' => $staff, 's' => $todayStart, 'e' => $todayEnd])
->getArrayResult();
+ /**
+ * کارِ امروزِ اپراتور جلسهٔ درمان است، نه نوبتِ اختصاصیافته.
+ *
+ * `appointments.staff_id` را هیچ مسیرِ نوبتدهی بهصورت پیشفرض پر نمیکند، پس
+ * کوئری بالا برای اپراتوری که تمام روز جلسه دارد هم صفر برمیگرداند. صف واقعی
+ * همان قاعدهٔ `findTodayForStaff` است — یک منبعِ حقیقت، نه دو تا.
+ */
+ $todaySessions = array_map(
+ static fn (TreatmentSession $s): array => [
+ 'uuid' => $s->getUuid(),
+ 'session_number'=> $s->getSessionNumber(),
+ 'total_sessions'=> $s->getTreatmentCase()->getTotalSessions(),
+ 'status' => $s->getStatus(),
+ 'service_name' => $s->getTreatmentCase()->getServiceItem()->getName(),
+ 'patient_name' => $s->getAppointment()?->getPatientName(),
+ 'slot_start' => $s->getAppointment()?->getSlotStart(),
+ ],
+ $this->treatmentSessionRepo->findTodayForStaff($staff, $todayStart, $todayEnd),
+ );
+
+ /**
+ * سرویسهای اپراتور از دو جا میآیند: تخصیص مستقیم، و پروتکلِ «طول درمان» که
+ * او را مجاز دانسته. بدون دومی، اپراتورِ لیزر که تمام کارش از پروتکل میآید
+ * «هیچ سرویسی ندارید» میدید.
+ */
+ $byAssignment = $this->serviceItemRepo->findByStaff($staff);
+ $byProtocol = $this->protocolRepo->findServicesForStaff($staff);
+
+ $merged = [];
+ foreach ([...$byAssignment, ...$byProtocol] as $item) {
+ $merged[$item->getUuid()] = $item;
+ }
+ ksort($merged);
+
$services = array_map(
static fn(ServiceItem $item) => [
'uuid' => $item->getUuid(),
@@ -734,7 +773,7 @@ class DashboardController extends BaseController
'price_rials' => $item->getPriceRials(),
'duration_minutes' => $item->getDurationMinutes(),
],
- $this->serviceItemRepo->findByStaff($staff),
+ array_values($merged),
);
return $this->success([
@@ -751,10 +790,12 @@ class DashboardController extends BaseController
],
'permissions' => StaffPermissions::DEFAULT,
'stats' => [
+ 'today_sessions' => count($todaySessions),
'today_appointments' => count($todayAppts),
'services' => count($services),
],
'services' => $services,
+ 'today_sessions' => $todaySessions,
'today_appointments' => $todayAppts,
]);
}
diff --git a/src/Treatment/Repository/TreatmentProtocolRepository.php b/src/Treatment/Repository/TreatmentProtocolRepository.php
index 866e7cb7..32df6f5e 100644
--- a/src/Treatment/Repository/TreatmentProtocolRepository.php
+++ b/src/Treatment/Repository/TreatmentProtocolRepository.php
@@ -3,6 +3,7 @@
namespace App\Treatment\Repository;
use App\ClinicService\Entity\ServiceItem;
+use App\Staff\Entity\ClinicStaff;
use App\Treatment\Entity\TreatmentProtocol;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@@ -27,4 +28,36 @@ class TreatmentProtocolRepository extends ServiceEntityRepository
{
return $this->findOneBy(['serviceItem' => $service, 'active' => true]);
}
+
+ /**
+ * سرویسهایی که پروتکلِ فعالشان این پرسنل را مجاز دانسته.
+ *
+ * تخصیصِ مستقیمِ سرویس (`ServiceItemRepository::findByStaff`) اینها را نمیبیند،
+ * ولی اپراتوری که تمام کارش از پروتکل میآید واقعاً همین سرویسها را انجام میدهد.
+ *
+ * @return list
+ */
+ public function findServicesForStaff(ClinicStaff $staff): array
+ {
+ // ریشه ServiceItem است نه پروتکل: Doctrine اجازه نمیدهد entityِ joinشده را
+ // بدون alias ریشه select کنی.
+ return $this->getEntityManager()->createQueryBuilder()
+ ->select('si')
+ ->from(ServiceItem::class, 'si')
+ ->join(TreatmentProtocol::class, 'p', 'WITH', 'p.serviceItem = si')
+ ->join('p.allowedStaff', 'ps')
+ ->join('si.section', 'sec')
+ ->where('ps.staff = :staff')
+ ->andWhere('p.active = true')
+ ->andWhere('si.active = true')
+ // محیط، وگرنه پرسنلِ یک کلینیک سرویس کلینیک دیگر را میبیند.
+ ->andWhere('sec.entityType = :type')
+ ->andWhere('sec.entityId = :id')
+ ->setParameter('staff', $staff)
+ ->setParameter('type', $staff->getEntityType())
+ ->setParameter('id', $staff->getEntityId())
+ ->orderBy('si.name', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
}