feat: Implement resource booking functionality

- Add service timeline builder for appointments to manage available slots.
- Create a hook to fetch resource booking services with effective durations.
- Develop ResourceBookingSlotController to handle API requests for resource booking slots.
- Implement ResourceBookingSlotService to calculate available time slots based on resource occupancy and service durations.
- Add tests for resource appointment creation and booking slot functionality to ensure correct behavior and edge cases.
This commit is contained in:
hamed
2026-08-03 14:34:23 +03:30
parent 981261ed3a
commit 4f69bc9044
21 changed files with 2045 additions and 514 deletions
@@ -113,6 +113,46 @@ class AppointmentRepository extends ServiceEntityRepository
return $this->occupiedIntervals($doctor, $from, $to, true, $excludeId);
}
/**
* بازه‌های اشغال‌شدهٔ یک **منبع** در پنجرهٔ [$from, $to).
*
* همان معیارِ نوبتِ پزشک، ولی محورش `resource_id` است: منبع می‌تواند نوبت‌هایی از
* چند پزشک داشته باشد، پس فیلترِ پزشک اینجا هم غلط است و هم ناقص.
*
* ردیف‌های `resource_occupancy` اینجا نمی‌آیند — آن‌ها مسیرِ موتور منبع‌محورند و
* {@see \App\Resource\Service\ResourceBookingSlotService} هر دو را کنار هم می‌گذارد.
*
* @return list<array{start: int, end: int}> مرتب‌شده بر اساس start
*/
public function findResourceBusyIntervals(int $resourceId, int $from, int $to, ?int $excludeId = null): array
{
$qb = $this->createQueryBuilder('a')
->select('a.slotStart AS start, a.slotEnd AS end')
->where('IDENTITY(a.resource) = :resource')
->andWhere('a.slotStart < :to')
->andWhere('a.slotEnd > :from')
->andWhere('a.isReserve = false')
->andWhere(
'a.status IN (:blocking) OR (a.status = :pending AND (a.expiresAt IS NULL OR a.expiresAt > :now))'
)
->setParameter('resource', $resourceId)
->setParameter('blocking', Appointment::SLOT_BLOCKING_STATUSES)
->setParameter('pending', Appointment::STATUS_PENDING)
->setParameter('now', time())
->setParameter('from', $from)
->setParameter('to', $to)
->orderBy('a.slotStart', 'ASC');
if ($excludeId !== null) {
$qb->andWhere('a.id != :excludeId')->setParameter('excludeId', $excludeId);
}
return array_map(
static fn (array $r): array => ['start' => (int) $r['start'], 'end' => (int) $r['end']],
$qb->getQuery()->getScalarResult(),
);
}
/**
* همان مجموعه‌ای که isSlotTaken() یک اسلات را با آن می‌سنجد — شامل نوبت‌های
* رزروی. برای پیمایش چندروزه که نمی‌خواهد به ازای هر اسلات یک کوئری بزند.