diff --git a/source/packages/plg_system_mokosuitebeauty/src/Helper/BookingHelper.php b/source/packages/plg_system_mokosuitebeauty/src/Helper/BookingHelper.php index 6a3a447..dcc34ca 100644 --- a/source/packages/plg_system_mokosuitebeauty/src/Helper/BookingHelper.php +++ b/source/packages/plg_system_mokosuitebeauty/src/Helper/BookingHelper.php @@ -82,18 +82,52 @@ class BookingHelper { $db = Factory::getContainer()->get(DatabaseInterface::class); - $booking = (object) [ - 'client_contact_id' => $clientContactId, - 'stylist_id' => $stylistId, - 'service_id' => $serviceId, - 'start_time' => $datetime, - 'status' => 'confirmed', - 'created_at' => Factory::getDate()->toSql(), - ]; + // Get service duration for overlap check + $db->setQuery($db->getQuery(true)->select('duration_minutes')->from('#__mokosuitebeauty_services')->where('id = ' . (int) $serviceId)); + $duration = (int) $db->loadResult(); - $db->insertObject('#__mokosuitebeauty_bookings', $booking, 'id'); + if ($duration <= 0) { + return (object) ['success' => false, 'error' => 'Invalid service']; + } - return (object) ['success' => true, 'booking_id' => (int) $booking->id]; + $db->transactionStart(); + + try { + // Check for overlapping bookings with row lock + $db->setQuery( + 'SELECT COUNT(*) FROM #__mokosuitebeauty_bookings b' + . ' JOIN #__mokosuitebeauty_services s ON s.id = b.service_id' + . ' WHERE b.stylist_id = ' . (int) $stylistId + . ' AND b.status NOT IN (' . $db->quote('cancelled') . ',' . $db->quote('no_show') . ')' + . ' AND b.start_time < DATE_ADD(' . $db->quote($datetime) . ', INTERVAL ' . $duration . ' MINUTE)' + . ' AND DATE_ADD(b.start_time, INTERVAL s.duration_minutes MINUTE) > ' . $db->quote($datetime) + . ' FOR UPDATE' + ); + $conflicts = (int) $db->loadResult(); + + if ($conflicts > 0) { + $db->transactionRollback(); + return (object) ['success' => false, 'error' => 'Time slot is no longer available']; + } + + $booking = (object) [ + 'client_contact_id' => $clientContactId, + 'stylist_id' => $stylistId, + 'service_id' => $serviceId, + 'start_time' => $datetime, + 'status' => 'confirmed', + 'created_at' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitebeauty_bookings', $booking, 'id'); + + $db->transactionCommit(); + + return (object) ['success' => true, 'booking_id' => (int) $booking->id]; + } catch (\Throwable $e) { + $db->transactionRollback(); + return (object) ['success' => false, 'error' => 'Booking failed']; + } } /** diff --git a/source/packages/plg_system_mokosuitebeauty/src/Helper/WalkInHelper.php b/source/packages/plg_system_mokosuitebeauty/src/Helper/WalkInHelper.php index 939c205..d194ca6 100644 --- a/source/packages/plg_system_mokosuitebeauty/src/Helper/WalkInHelper.php +++ b/source/packages/plg_system_mokosuitebeauty/src/Helper/WalkInHelper.php @@ -77,7 +77,7 @@ class WalkInHelper ->select('COALESCE(SUM(s.duration_minutes), 0)') ->from($db->quoteName('#__mokosuitebeauty_walkins', 'w')) ->join('LEFT', $db->quoteName('#__mokosuitebeauty_services', 's') . ' ON s.id = w.service_id') - ->where($db->quoteName('w.status') . ' IN (' . $db->quote('waiting') . ',' . $db->quote('in_service') . ')')); + ->where($db->quoteName('w.status') . ' = ' . $db->quote('waiting'))); $totalMinutes = (int) $db->loadResult();