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:
hamed
2026-07-15 23:15:45 +03:30
parent 6904361e32
commit 5937f7e176
16 changed files with 817 additions and 26 deletions
@@ -176,6 +176,9 @@ class ClinicServiceController extends BaseController
$dm = $data['duration_minutes'];
$item->setDurationMinutes(($dm === null || $dm === '') ? null : (int) $dm);
}
if (array_key_exists('bookable', $data)) {
$item->setBookable((bool) $data['bookable']);
}
$this->itemRepo->save($item);
@@ -217,6 +220,9 @@ class ClinicServiceController extends BaseController
$dm = $data['duration_minutes'];
$item->setDurationMinutes(($dm === null || $dm === '') ? null : (int) $dm);
}
if (array_key_exists('bookable', $data)) {
$item->setBookable((bool) $data['bookable']);
}
$this->itemRepo->save($item);
+7
View File
@@ -58,6 +58,10 @@ class ServiceItem
#[ORM\Column(name: 'duration_minutes', type: 'integer', nullable: true)]
private ?int $durationMinutes = null;
/** نمایش این سرویس در نوبت‌دهی (پزشک ممکن است همهٔ سرویس‌ها را ارائه ندهد). */
#[ORM\Column(type: 'boolean', options: ['default' => false])]
private bool $bookable = false;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
@@ -85,6 +89,7 @@ class ServiceItem
public function isInsuranceCovered(): bool { return $this->insuranceCovered; }
public function getInsurancePriceRials(): ?int { return $this->insurancePriceRials; }
public function getDurationMinutes(): ?int { return $this->durationMinutes; }
public function isBookable(): bool { return $this->bookable; }
public function getCreatedAt(): int { return $this->createdAt; }
public function getUpdatedAt(): int { return $this->updatedAt; }
@@ -122,6 +127,7 @@ class ServiceItem
public function setInsuranceCovered(bool $v): self { $this->insuranceCovered = $v; $this->updatedAt = time(); return $this; }
public function setInsurancePriceRials(?int $v): self { $this->insurancePriceRials = $v; $this->updatedAt = time(); return $this; }
public function setDurationMinutes(?int $v): self { $this->durationMinutes = $v; $this->updatedAt = time(); return $this; }
public function setBookable(bool $v): self { $this->bookable = $v; $this->updatedAt = time(); return $this; }
public function toArray(): array
{
@@ -151,6 +157,7 @@ class ServiceItem
'insurance_covered' => $this->insuranceCovered,
'insurance_price_rials' => $this->insurancePriceRials,
'duration_minutes' => $this->durationMinutes,
'bookable' => $this->bookable,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt,
];
@@ -57,6 +57,25 @@ class ServiceItemRepository extends ServiceEntityRepository
return $counts;
}
/**
* تعداد سرویس‌های فعالِ «نمایش در نوبت‌دهی» (bookable) متعلق به یک entity
* (پزشک/کلینیک) — از طریق section.entityType/entityId. برای اجبارِ حالت سرویس.
*/
public function countBookableByEntity(string $entityType, int $entityId): int
{
return (int) $this->createQueryBuilder('i')
->select('COUNT(i.id)')
->join('i.section', 's')
->where('s.entityType = :type')
->andWhere('s.entityId = :id')
->andWhere('i.bookable = true')
->andWhere('i.active = true')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->getQuery()
->getSingleScalarResult();
}
public function save(ServiceItem $item): void
{
$this->getEntityManager()->persist($item);