feat: add service-based booking mode to appointment scheduling
- Introduced a new booking mode in WeeklySchedule to support service-based appointments. - Updated SlotCalculatorService to calculate available start times based on selected service durations and buffer times. - Enhanced AppointmentController to handle service items during booking, calculating slot_end on the server side. - Implemented validation to ensure at least one bookable service exists for doctors in service mode. - Added new API endpoint to retrieve available appointment slots based on selected services. - Updated MyAppointmentsController to accept service items during appointment creation. - Modified ServiceItem entity to include a bookable flag, allowing services to be marked for scheduling. - Created migration to add bookable column to service_items table. - Added tests for service-based slot calculations and validation logic.
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Auth\Entity\User;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -37,6 +38,13 @@ class AppointmentRepository extends ServiceEntityRepository
|
||||
$start = $appointment->getSlotStart();
|
||||
$end = $appointment->getSlotEnd();
|
||||
|
||||
// قفلِ per-doctor (SELECT ... FOR UPDATE روی ردیف پزشک): رزروهای
|
||||
// همزمانِ یک پزشک را سریالایز میکند. در حالت نوبتدهی سرویسی که
|
||||
// نوبتها طول متغیر و شروعِ متفاوت دارند، unique-keyِ (doctor,slot_start)
|
||||
// تداخلِ بازهایِ دو رزروِ همزمان را نمیگیرد؛ این قفل تضمین میکند
|
||||
// بررسیِ isSlotTaken و insert بهصورت اتمیک نسبت به سایر رزروها انجام شود.
|
||||
$em->lock($doctor, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
if ($this->isSlotTaken($doctor, $start, $end)) {
|
||||
throw new SlotTakenException();
|
||||
}
|
||||
@@ -87,6 +95,37 @@ class AppointmentRepository extends ServiceEntityRepository
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* بازههای اشغالشدهٔ یک پزشک در پنجرهٔ [$from, $to) — برای محاسبهٔ زمانِ خالی
|
||||
* در حالت نوبتدهی سرویسی. همان معیارِ isSlotTaken (blocking یا pendingِ زنده)،
|
||||
* ولی نوبتهای «آزاد» (is_reserve) هیچ بازهای اشغال نمیکنند.
|
||||
*
|
||||
* @return array<array{start:int,end:int}> مرتبشده بر اساس start
|
||||
*/
|
||||
public function findBusyIntervals(Doctor $doctor, int $from, int $to): array
|
||||
{
|
||||
$rows = $this->createQueryBuilder('a')
|
||||
->select('a.slotStart AS start, a.slotEnd AS end')
|
||||
->where('a.doctor = :doctor')
|
||||
->andWhere('a.isReserve = false')
|
||||
->andWhere('a.slotStart < :to')
|
||||
->andWhere('a.slotEnd > :from')
|
||||
->andWhere(
|
||||
'a.status IN (:blocking) OR (a.status = :pending AND (a.expiresAt IS NULL OR a.expiresAt > :now))'
|
||||
)
|
||||
->setParameter('doctor', $doctor)
|
||||
->setParameter('blocking', Appointment::SLOT_BLOCKING_STATUSES)
|
||||
->setParameter('pending', Appointment::STATUS_PENDING)
|
||||
->setParameter('now', time())
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to)
|
||||
->orderBy('a.slotStart', 'ASC')
|
||||
->getQuery()
|
||||
->getScalarResult();
|
||||
|
||||
return array_map(fn($r) => ['start' => (int) $r['start'], 'end' => (int) $r['end']], $rows);
|
||||
}
|
||||
|
||||
/** Check if a slot is already taken (confirmed or pending) */
|
||||
public function isSlotTaken(Doctor $doctor, int $slotStart, int $slotEnd, ?int $excludeId = null): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user