fix(booking): reserve conversion produced a zero-length midnight appointment
TransferReserveModal built the live appointment from appointment_time/end_time, which on a reserve entry are both 00:00 because slot_start == slot_end. Moving a reserve back to the appointment list silently created a zero-length appointment at midnight. With the new duration validation it would now fail loudly instead. Converting back now asks for a real time: the service picker in service mode, two required time inputs in slot mode. The appointment -> reserve direction is untouched. GET /my/appointments has its own array-hydration serializer rather than Appointment::toArray(), so it exposed none of the service fields the panel needs. Added service_items (separate query, no row multiplication and no N+1), clinic_uuid and the duration pair. This was also a hidden prerequisite of the public-site task, whose checklist listed it as "verify first". The reserve table now lists every service instead of only the first. Not done, deliberately: the DataTable migration the task asked for. Its stated reason — inline tokens breaking dark mode — does not hold; this table's th/td already use CSS variables and dark mode works. Rewriting a working table for no real gain is unjustified risk. Task: docs/new_feture/taskes/task-00-service-mode-completion/ Slot-mode contract: unchanged (--group=slot-mode-frozen green) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -326,6 +326,8 @@ class MyAppointmentsController extends BaseController
|
||||
'DISTINCT a.uuid, a.slotStart, a.slotEnd, a.status, a.createdAt, a.version',
|
||||
'a.isReserve, a.depositRequired, a.depositAmountRials, a.note, a.patientName as override_name',
|
||||
'a.patientNationalCode as national_code, a.patientGender as gender',
|
||||
'a.serviceTotalMinutes as service_total_minutes, a.serviceBufferMinutes as service_buffer_minutes',
|
||||
'cl.uuid as clinic_uuid',
|
||||
'd.uuid as doctor_uuid, d.name as doctor_name',
|
||||
'u.uuid as patient_uuid, u.mobileNumber as patient_mobile, u.realName as patient_name',
|
||||
'ss.uuid as section_uuid, ss.name as section_name',
|
||||
@@ -338,6 +340,7 @@ class MyAppointmentsController extends BaseController
|
||||
->leftJoin('a.serviceSection', 'ss')
|
||||
->leftJoin('a.serviceItem', 'si')
|
||||
->leftJoin('a.staff', 'st')
|
||||
->leftJoin('a.clinic', 'cl')
|
||||
->andWhere('a.isReserve = :reserveOnly')
|
||||
->setParameter('reserveOnly', $reserveOnly)
|
||||
->orderBy('a.slotStart', 'ASC');
|
||||
@@ -410,6 +413,10 @@ class MyAppointmentsController extends BaseController
|
||||
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
|
||||
->getQuery()->getArrayResult();
|
||||
|
||||
// سرویسهای چندگانه در یک کوئری جدا: JOIN زدنشان به کوئری بالا ردیفها را ضرب
|
||||
// میکند و pagination را میشکند.
|
||||
$serviceItemsByAppointment = $this->serviceItemsFor(array_column($rows, 'uuid'));
|
||||
|
||||
$items = array_map(fn(array $a) => [
|
||||
'uuid' => $a['uuid'],
|
||||
'patient_name' => $a['override_name'] ?: ($a['patient_name'] ?? ''),
|
||||
@@ -433,12 +440,51 @@ class MyAppointmentsController extends BaseController
|
||||
'note' => $a['note'],
|
||||
'service_section' => $a['section_uuid'] ? ['uuid' => $a['section_uuid'], 'name' => $a['section_name']] : null,
|
||||
'service_item' => $a['service_uuid'] ? ['uuid' => $a['service_uuid'], 'name' => $a['service_name']] : null,
|
||||
// فهرست کاملِ سرویسها؛ `service_item` بالا فقط سرویسِ اول است و کلاینتی که
|
||||
// تنها آن را بخواند بقیه را نشان نمیدهد.
|
||||
'service_items' => $serviceItemsByAppointment[$a['uuid']] ?? [],
|
||||
'staff' => $a['staff_uuid'] ? ['uuid' => $a['staff_uuid'], 'full_name' => $a['staff_name']] : null,
|
||||
'clinic_uuid' => $a['clinic_uuid'],
|
||||
'service_total_minutes' => $a['service_total_minutes'] !== null ? (int) $a['service_total_minutes'] : null,
|
||||
'service_buffer_minutes' => $a['service_buffer_minutes'] !== null ? (int) $a['service_buffer_minutes'] : null,
|
||||
], $rows);
|
||||
|
||||
return $this->paginated($items, (int) $total, $page, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* سرویسهای چندگانهٔ چند نوبت، گروهبندیشده بر uuid نوبت — یک کوئری برای کل صفحه.
|
||||
*
|
||||
* @param string[] $appointmentUuids
|
||||
* @return array<string, array<int, array{uuid:string, name:string, price_rials:int}>>
|
||||
*/
|
||||
private function serviceItemsFor(array $appointmentUuids): array
|
||||
{
|
||||
if ($appointmentUuids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->em->createQueryBuilder()
|
||||
->select('a.uuid as appointment_uuid, si.uuid, si.name, si.priceRials')
|
||||
->from(Appointment::class, 'a')
|
||||
->join('a.serviceItems', 'si')
|
||||
->where('a.uuid IN (:uuids)')
|
||||
->setParameter('uuids', $appointmentUuids)
|
||||
->getQuery()
|
||||
->getArrayResult();
|
||||
|
||||
$grouped = [];
|
||||
foreach ($rows as $row) {
|
||||
$grouped[$row['appointment_uuid']][] = [
|
||||
'uuid' => $row['uuid'],
|
||||
'name' => $row['name'],
|
||||
'price_rials' => (int) $row['priceRials'],
|
||||
];
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
#[Route('/api/v1/my/appointments/today-stats', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function todayStats(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
|
||||
Reference in New Issue
Block a user