Multi-slot booking system with conflict detection and approval workflow for room reservations
The reservation system is the core feature of Apartado de Salas, enabling users to book rooms for events across multiple time slots with material assignment and administrative approval.
// From app/controllers/ReservationController.php:66-67$db = Database::getConnection();$db->beginTransaction();
5
Create reservation record
The main reservation (expediente) is created.
// From app/models/Reservation.php:17-38public function create( int $userId, int $roomId, string $eventName, ?string $notes = null): int { $sql = "INSERT INTO reservations (user_id, room_id, event_name, notes) VALUES (:user_id, :room_id, :event_name, :notes)"; $stmt->execute(); return (int) $this->db->lastInsertId();}
6
Validate and create time slots
Each time slot is validated for conflicts before creation.
// From app/controllers/ReservationController.php:78-91foreach ($dates as $index => $date) { if ($slotModel->hasConflict((int)$roomId, $date, $start, $end)) { throw new Exception("Conflicto de horario el $date de $start a $end."); } $slotModel->create($reservationId, $date, $start, $end);}
7
Assign materials
Materials are validated and attached to the reservation.
// From app/controllers/ReservationController.php:94-98if (!$materialModel->validateForRoom($materials, (int)$roomId)) { throw new Exception('Materiales no válidos para la sala.');}$reservationModel->attachMaterials($reservationId, $materials);
8
Commit transaction
If all operations succeed, the transaction is committed.
// From app/controllers/ReservationController.php:102-106$db->commit();Session::setFlash('success', 'La reservación fue creada correctamente.');header('Location:'. BASE_URL.'/dashboard');
If any step fails, the entire transaction is rolled back to maintain data consistency:
The system prevents double-booking by detecting time slot conflicts:
Conflict Detection Algorithm
// From app/models/ReservationSlot.php:59-85public function hasConflict( int $roomId, string $date, string $startTime, string $endTime): bool { $sql = "SELECT 1 FROM reservation_slots rs JOIN reservations r ON r.id = rs.reservation_id WHERE r.room_id = :room_id AND rs.date = :date AND (rs.start_time < :end_time AND rs.end_time > :start_time) LIMIT 1"; return (bool) $stmt->fetchColumn();}
Logic: A conflict exists if:
Same room
Same date
Time ranges overlap: (existing_start < new_end) AND (existing_end > new_start)
// From app/models/ReservationSlot.php:40-54public function getByReservation(int $reservationId): array{ $sql = "SELECT date, start_time, end_time FROM reservation_slots WHERE reservation_id = :reservation_id ORDER BY date, start_time"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}
// From app/models/ReservationSlot.php:91-108public function getOccupiedSlots(int $roomId, string $date): array{ $sql = "SELECT rs.start_time, rs.end_time FROM reservation_slots rs JOIN reservations r ON r.id = rs.reservation_id WHERE r.room_id = :room_id AND rs.date = :date ORDER BY rs.start_time"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}
This method is useful for displaying availability calendars.
// From app/models/Reservation.php:101-120public function updateStatus(int $reservationId, string $status): bool{ $allowedStatuses = ['pendiente', 'aprobado', 'rechazado']; if (!in_array($status, $allowedStatuses, true)) { throw new InvalidArgumentException('Estado inválido'); } $sql = "UPDATE reservations SET status = :status WHERE id = :id"; return $stmt->execute();}
The system provides multiple methods to retrieve reservations:
Get All Reservations
// From app/models/Reservation.php:76-96public function getAll(): array{ $sql = "SELECT r.id, r.event_name, r.status, r.created_at, rm.name AS room_name, u.username FROM reservations r JOIN rooms rm ON rm.id = r.room_id JOIN users u ON u.id = r.user_id ORDER BY r.created_at DESC"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}
Used by admins to view all reservations in the system.
Get by Status
// From app/models/Reservation.php:179-207public function getByStatus(string $status): array{ $allowedStatuses = ['pendiente', 'aprobado', 'rechazado']; if (!in_array($status, $allowedStatuses, true)) { return []; } $sql = "SELECT /* ... */ WHERE r.status = :status ORDER BY r.created_at DESC"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}
// From app/models/Reservation.php:212-231public function getByUser(int $userId): array{ $sql = "SELECT r.id, r.event_name, r.status, r.created_at, rm.name AS room_name FROM reservations r JOIN rooms rm ON rm.id = r.room_id WHERE r.user_id = :user_id ORDER BY r.created_at DESC"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}
Used in the user dashboard to show personal reservations.
Find by ID
// From app/models/Reservation.php:43-70public function findById(int $id): ?array{ $sql = "SELECT r.id, r.user_id, r.room_id, r.event_name, r.status, r.notes, r.created_at, u.username, rm.name AS room_name FROM reservations r JOIN users u ON u.id = r.user_id JOIN rooms rm ON rm.id = r.room_id WHERE r.id = :id LIMIT 1"; return $reservation ?: null;}
// From app/models/Reservation.php:236-249public function getSlots(int $reservationId): array{ $sql = "SELECT date, start_time, end_time FROM reservation_slots WHERE reservation_id = :id ORDER BY date, start_time"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}
// From app/models/Reservation.php:159-174public function getMaterials(int $reservationId): array{ $sql = "SELECT m.id, m.name FROM reservation_materials rm JOIN materials m ON m.id = rm.material_id WHERE rm.reservation_id = :reservation_id ORDER BY m.name"; return $stmt->fetchAll(PDO::FETCH_ASSOC);}