diff --git a/backend/hotel_hacienda/src/app.js b/backend/hotel_hacienda/src/app.js
index e140b56..84f0350 100644
--- a/backend/hotel_hacienda/src/app.js
+++ b/backend/hotel_hacienda/src/app.js
@@ -36,6 +36,7 @@ const restaurantRoutes = require('./routes/restaurantpl.routes');
const incomehrxRoutes = require('./routes/incomehrx.routes');
const roomsRoutes = require('./routes/rooms.routes');
const dashboardRoutes = require('./routes/dashboard.routes');
+const reservationsRoutes = require('./routes/reservations.routes');
//Prefijo - Auth routes are public (no middleware)
app.use('/api/auth', authRoutes);
@@ -58,5 +59,6 @@ app.use('/api/hotelpl', authMiddleware, hotelRoutes);
app.use('/api/restaurantpl', authMiddleware, restaurantRoutes);
app.use('/api/rooms', authMiddleware, roomsRoutes);
app.use('/api/dashboard', authMiddleware, dashboardRoutes);
+app.use('/api/reservations', authMiddleware, reservationsRoutes);
module.exports = app;
diff --git a/backend/hotel_hacienda/src/controllers/reservations.controller.js b/backend/hotel_hacienda/src/controllers/reservations.controller.js
new file mode 100644
index 0000000..5b08b8a
--- /dev/null
+++ b/backend/hotel_hacienda/src/controllers/reservations.controller.js
@@ -0,0 +1,191 @@
+const pool = require('../db/connection');
+
+const getReservations = async (req, res) => {
+ try {
+ const { status, from_date, to_date, search } = req.query;
+ let query = `
+ SELECT r.*, g.first_name, g.last_name, g.phone, g.email,
+ rm.name_room, rm.bed_type
+ FROM reservations r
+ JOIN guests g ON g.id = r.guest_id
+ LEFT JOIN rooms rm ON rm.id_room = r.room_id
+ WHERE 1=1
+ `;
+ const params = [];
+ let paramIndex = 1;
+
+ if (status && status !== 'all') {
+ query += ` AND r.status = $${paramIndex++}`;
+ params.push(status);
+ }
+ if (from_date) {
+ query += ` AND r.check_in >= $${paramIndex++}`;
+ params.push(from_date);
+ }
+ if (to_date) {
+ query += ` AND r.check_out <= $${paramIndex++}`;
+ params.push(to_date);
+ }
+ if (search) {
+ query += ` AND (g.first_name ILIKE $${paramIndex} OR g.last_name ILIKE $${paramIndex})`;
+ params.push(`%${search}%`);
+ paramIndex++;
+ }
+ query += ' ORDER BY r.created_at DESC LIMIT 100';
+
+ const result = await pool.query(query, params);
+ res.json({ reservations: result.rows });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al obtener reservaciones' });
+ }
+};
+
+const createReservation = async (req, res) => {
+ try {
+ const { room_id, guest_id, guest, check_in, check_out, channel, total_amount, adults, children, notes } = req.body;
+ const created_by = req.user?.user_id;
+
+ // Create guest if new
+ let finalGuestId = guest_id;
+ if (!finalGuestId && guest) {
+ const guestResult = await pool.query(
+ 'INSERT INTO guests (first_name, last_name, email, phone, nationality) VALUES ($1, $2, $3, $4, $5) RETURNING id',
+ [guest.first_name, guest.last_name, guest.email || null, guest.phone || null, guest.nationality || null]
+ );
+ finalGuestId = guestResult.rows[0].id;
+ }
+
+ // Check availability
+ const overlap = await pool.query(
+ `SELECT id FROM reservations
+ WHERE room_id = $1 AND status IN ('confirmed', 'checked_in')
+ AND check_in < $3 AND check_out > $2`,
+ [room_id, check_in, check_out]
+ );
+ if (overlap.rows.length > 0) {
+ return res.status(409).json({ message: 'La habitacion no esta disponible para esas fechas' });
+ }
+
+ const result = await pool.query(
+ `INSERT INTO reservations (room_id, guest_id, check_in, check_out, status, channel, total_amount, adults, children, notes, created_by)
+ VALUES ($1, $2, $3, $4, 'pending', $5, $6, $7, $8, $9, $10) RETURNING *`,
+ [room_id, finalGuestId, check_in, check_out, channel || 'direct', total_amount, adults || 1, children || 0, notes, created_by]
+ );
+
+ res.status(201).json({ reservation: result.rows[0], message: 'Reservacion creada correctamente' });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al crear reservacion' });
+ }
+};
+
+const updateReservation = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { room_id, check_in, check_out, channel, total_amount, adults, children, notes } = req.body;
+
+ const result = await pool.query(
+ `UPDATE reservations SET room_id = COALESCE($1, room_id), check_in = COALESCE($2, check_in),
+ check_out = COALESCE($3, check_out), channel = COALESCE($4, channel),
+ total_amount = COALESCE($5, total_amount), adults = COALESCE($6, adults),
+ children = COALESCE($7, children), notes = COALESCE($8, notes), updated_at = NOW()
+ WHERE id = $9 RETURNING *`,
+ [room_id, check_in, check_out, channel, total_amount, adults, children, notes, id]
+ );
+
+ if (result.rows.length === 0) return res.status(404).json({ message: 'Reservacion no encontrada' });
+ res.json({ reservation: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar reservacion' });
+ }
+};
+
+const updateReservationStatus = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { status } = req.body;
+ const userId = req.user?.user_id;
+
+ const current = await pool.query('SELECT * FROM reservations WHERE id = $1', [id]);
+ if (current.rows.length === 0) return res.status(404).json({ message: 'Reservacion no encontrada' });
+
+ const reservation = current.rows[0];
+ const validTransitions = {
+ pending: ['confirmed', 'cancelled'],
+ confirmed: ['checked_in', 'cancelled'],
+ checked_in: ['checked_out'],
+ };
+
+ const allowed = validTransitions[reservation.status];
+ if (!allowed || !allowed.includes(status)) {
+ return res.status(400).json({ message: `No se puede cambiar de ${reservation.status} a ${status}` });
+ }
+
+ await pool.query('UPDATE reservations SET status = $1, updated_at = NOW() WHERE id = $2', [status, id]);
+
+ // Cascading side effects
+ if (status === 'checked_in') {
+ // Set room to occupied
+ await pool.query("UPDATE rooms SET status = 'occupied' WHERE id_room = $1", [reservation.room_id]);
+ await pool.query(
+ 'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',
+ [reservation.room_id, 'available', 'occupied', userId]
+ );
+ // Create guest stay record
+ await pool.query(
+ 'INSERT INTO guest_stays (guest_id, reservation_id, room_id, check_in) VALUES ($1, $2, $3, NOW())',
+ [reservation.guest_id, reservation.id, reservation.room_id]
+ );
+ }
+
+ if (status === 'checked_out') {
+ // Set room to cleaning
+ await pool.query("UPDATE rooms SET status = 'cleaning' WHERE id_room = $1", [reservation.room_id]);
+ await pool.query(
+ 'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',
+ [reservation.room_id, 'occupied', 'cleaning', userId]
+ );
+ // Create housekeeping task
+ await pool.query(
+ `INSERT INTO housekeeping_tasks (room_id, priority, type, status) VALUES ($1, 'high', 'checkout', 'pending')`,
+ [reservation.room_id]
+ );
+ // Close guest stay
+ await pool.query(
+ 'UPDATE guest_stays SET check_out = NOW(), total_charged = $1 WHERE reservation_id = $2 AND check_out IS NULL',
+ [reservation.total_amount, reservation.id]
+ );
+ }
+
+ if (status === 'cancelled') {
+ // Free room if it was occupied
+ if (reservation.status === 'checked_in') {
+ await pool.query("UPDATE rooms SET status = 'available' WHERE id_room = $1", [reservation.room_id]);
+ }
+ }
+
+ res.json({ message: 'Estado de reservacion actualizado' });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar estado' });
+ }
+};
+
+const checkAvailability = async (req, res) => {
+ try {
+ const { room_id, check_in, check_out } = req.query;
+ const overlap = await pool.query(
+ `SELECT id FROM reservations WHERE room_id = $1 AND status IN ('confirmed', 'checked_in')
+ AND check_in < $3 AND check_out > $2`,
+ [room_id, check_in, check_out]
+ );
+ res.json({ available: overlap.rows.length === 0 });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al verificar disponibilidad' });
+ }
+};
+
+module.exports = { getReservations, createReservation, updateReservation, updateReservationStatus, checkAvailability };
diff --git a/backend/hotel_hacienda/src/routes/reservations.routes.js b/backend/hotel_hacienda/src/routes/reservations.routes.js
new file mode 100644
index 0000000..a4b6370
--- /dev/null
+++ b/backend/hotel_hacienda/src/routes/reservations.routes.js
@@ -0,0 +1,11 @@
+const express = require('express');
+const router = express.Router();
+const ctrl = require('../controllers/reservations.controller');
+
+router.get('/', ctrl.getReservations);
+router.get('/availability', ctrl.checkAvailability);
+router.post('/', ctrl.createReservation);
+router.put('/:id', ctrl.updateReservation);
+router.put('/:id/status', ctrl.updateReservationStatus);
+
+module.exports = router;
diff --git a/frontend/Frontend-Hotel/src/App.jsx b/frontend/Frontend-Hotel/src/App.jsx
index e55cc78..f75cabb 100644
--- a/frontend/Frontend-Hotel/src/App.jsx
+++ b/frontend/Frontend-Hotel/src/App.jsx
@@ -40,6 +40,8 @@ import NewMonthlyPayment from "./pages/Expenses/NewMonthlyPayment.jsx";
import Outcomes from "./pages/Inventory/Outcomes.jsx";
import HousekeeperOutcomes from "./pages/Inventory/HousekeeperOutcomes.jsx";
import RoomDashboard from "./pages/RoomDashboard/RoomDashboard.jsx";
+import Reservations from "./pages/Reservations/Reservations.jsx";
+import NewReservation from "./pages/Reservations/NewReservation.jsx";
import "./styles/global.css";
//SubmenĂș de Hotel
@@ -133,6 +135,8 @@ export default function App() {
{t("common.loading")}
+{t("common.noResults")}
+