fix(treatment): make the staff panel a queue instead of an assignment list

findTodayForStaff filtered on appointments.staff, and no booking path ever sets
that column. The result was a staff panel that was empty for every operator, in
every tenant, no matter how much work the day held.

Today's sessions now reach an operator three ways: the session they already
claimed (performedBy), the appointment a secretary pre-assigned to them, or
unclaimed work whose protocol names them. A protocol with no staff list means
everyone may perform it — the same "no rows is not a restriction" rule
ResourceServiceOffering already uses.

The query is also tenant-scoped, which the old one was not: it relied on
appointments.staff being a same-tenant row rather than saying so.

Verified against the dev database: the operator behind 09128726723 now gets
both of today's sessions on "لیزر توتال", which the old query returned none of.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 12:34:01 +03:30
co-authored by Claude Opus 5
parent f5902eeeeb
commit 38ea477429
2 changed files with 250 additions and 2 deletions
@@ -88,16 +88,53 @@ class TreatmentSessionRepository extends ServiceEntityRepository
/**
* جلسات امروزِ یک پرسنل — از روی نوبتِ متصل، نه از روی سررسید تخمینی.
*
* صف است، نه فهرستِ اختصاص‌یافته. سه چیز اینجا می‌آید:
* - جلسه‌ای که همین پرسنل برداشته (`performedBy`)،
* - نوبتی که منشی از قبل به او داده (`appointment.staff`)،
* - و جلسهٔ بی‌صاحبی که پروتکلش او را مجاز می‌داند.
*
* فیلترِ قبلی فقط شرط دوم را داشت، و چون هیچ مسیرِ نوبت‌دهی اپراتور را ست
* نمی‌کند، پنل پرسنل همیشه خالی بود.
*
* پروتکلِ بدون فهرست پرسنل یعنی «همه مجازند» — همان قاعدهٔ
* `ResourceServiceOffering` که نبودِ رکورد را محدودیت حساب نمی‌کند.
*
* @return TreatmentSession[]
*/
public function findTodayForStaff(ClinicStaff $staff, int $dayStart, int $dayEnd): array
{
$allowsThisStaff = <<<'DQL'
EXISTS (
SELECT 1 FROM App\Treatment\Entity\TreatmentProtocolStaff ps
JOIN ps.protocol p
WHERE p.serviceItem = c.serviceItem AND p.active = true AND ps.staff = :staff
)
DQL;
$namesAnyStaff = <<<'DQL'
EXISTS (
SELECT 1 FROM App\Treatment\Entity\TreatmentProtocolStaff ps2
JOIN ps2.protocol p2
WHERE p2.serviceItem = c.serviceItem AND p2.active = true
)
DQL;
return $this->createQueryBuilder('s')
->join('s.appointment', 'a')
->where('a.staff = :staff')
->andWhere('a.slotStart >= :from')
->join('s.treatmentCase', 'c')
->where('a.slotStart >= :from')
->andWhere('a.slotStart <= :to')
// محیط، وگرنه پرسنلِ یک کلینیک جلسات کلینیک دیگر را می‌بیند.
->andWhere('s.entityType = :type')
->andWhere('s.entityId = :id')
->andWhere(sprintf(
's.performedBy = :staff OR a.staff = :staff OR (s.performedBy IS NULL AND a.staff IS NULL AND (%s OR NOT %s))',
$allowsThisStaff,
$namesAnyStaff,
))
->setParameter('staff', $staff)
->setParameter('type', $staff->getEntityType())
->setParameter('id', $staff->getEntityId())
->setParameter('from', $dayStart)
->setParameter('to', $dayEnd)
->orderBy('a.slotStart', 'ASC')