From cfbdca238c2b7f1bdbdbfbb43965682e65e74b43 Mon Sep 17 00:00:00 2001 From: Martin Slachta Date: Tue, 16 Jun 2026 10:54:00 +0200 Subject: [PATCH] #19 - Merge timetable reservations --- includes/Services/RsvReservationService.php | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/includes/Services/RsvReservationService.php b/includes/Services/RsvReservationService.php index 1c4380c..7f274ab 100644 --- a/includes/Services/RsvReservationService.php +++ b/includes/Services/RsvReservationService.php @@ -27,6 +27,9 @@ class RsvReservationService { } public function create(RsvReservation $reservation) { + $reservation->timetable_reservations = + $this->merge_adjacent($reservation->timetable_reservations); + // Serialise the availability-check + insert per timetable so two // concurrent bookings for the last free slot can't both pass the // capacity check and oversell. Locks are taken in a stable order to @@ -80,6 +83,48 @@ class RsvReservationService { return Db::prefix() . 'rsv_booking_' . $timetable_id; } + /** + * Collapse runs of touching reservations into single spans, so a sequence + * of back-to-back blocks is stored and notified as one booking. + * + * @param list $timetable_reservations + * @return list + */ + private function merge_adjacent(array $timetable_reservations): array { + $by_timetable = []; + foreach ($timetable_reservations as $tr) { + $by_timetable[$tr->timetable_id][] = $tr; + } + + $merged = []; + foreach ($by_timetable as $group) { + usort($group, fn($a, $b) => $a->start_utc <=> $b->start_utc); + + $current = null; + foreach ($group as $tr) { + if ($current !== null && $tr->start_utc <= $current->end_utc) { + if ($tr->end_utc > $current->end_utc) { + $current->end_utc = $tr->end_utc; + } + continue; + } + + if ($current !== null) { + $merged[] = $current; + } + $current = new RsvTimetableReservation( + null, $tr->timetable_id, $tr->start_utc, $tr->end_utc + ); + } + + if ($current !== null) { + $merged[] = $current; + } + } + + return $merged; + } + /** Delete a reservation and its dependent timetable reservations and confirmations. */ public function delete(int $id): void { $this->repo->delete($id);