diff --git a/backend/hotel_hacienda/src/app.js b/backend/hotel_hacienda/src/app.js index 96f2e8d..e140b56 100644 --- a/backend/hotel_hacienda/src/app.js +++ b/backend/hotel_hacienda/src/app.js @@ -34,6 +34,8 @@ const exchangeRoutes = require('./routes/exchange.routes'); const hotelRoutes = require('./routes/hotelpl.routes'); const restaurantRoutes = require('./routes/restaurantpl.routes'); const incomehrxRoutes = require('./routes/incomehrx.routes'); +const roomsRoutes = require('./routes/rooms.routes'); +const dashboardRoutes = require('./routes/dashboard.routes'); //Prefijo - Auth routes are public (no middleware) app.use('/api/auth', authRoutes); @@ -54,5 +56,7 @@ app.use('/api/purchases', authMiddleware, purchaseRoutes); app.use('/api/exchange', authMiddleware, exchangeRoutes); app.use('/api/hotelpl', authMiddleware, hotelRoutes); app.use('/api/restaurantpl', authMiddleware, restaurantRoutes); +app.use('/api/rooms', authMiddleware, roomsRoutes); +app.use('/api/dashboard', authMiddleware, dashboardRoutes); module.exports = app; diff --git a/backend/hotel_hacienda/src/controllers/dashboard.controller.js b/backend/hotel_hacienda/src/controllers/dashboard.controller.js new file mode 100644 index 0000000..1357c8c --- /dev/null +++ b/backend/hotel_hacienda/src/controllers/dashboard.controller.js @@ -0,0 +1,65 @@ +const pool = require('../db/connection'); + +const getKPIs = async (req, res) => { + try { + const today = new Date().toISOString().split('T')[0]; + + const [totalRooms, availableRooms, checkIns, checkOuts] = await Promise.all([ + pool.query('SELECT COUNT(*) as count FROM rooms'), + pool.query("SELECT COUNT(*) as count FROM rooms WHERE status = 'available'"), + pool.query("SELECT COUNT(*) as count FROM reservations WHERE check_in = $1 AND status IN ('confirmed', 'checked_in')", [today]), + pool.query("SELECT COUNT(*) as count FROM reservations WHERE check_out = $1 AND status = 'checked_in'", [today]), + ]); + + const total = parseInt(totalRooms.rows[0].count); + const available = parseInt(availableRooms.rows[0].count); + const occupancy = total > 0 ? Math.round(((total - available) / total) * 100) : 0; + + res.json({ + occupancy, + availableRooms: available, + totalRooms: total, + todayCheckIns: parseInt(checkIns.rows[0].count), + todayCheckOuts: parseInt(checkOuts.rows[0].count), + }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener KPIs' }); + } +}; + +const getWeeklyRevenue = async (req, res) => { + try { + const result = await pool.query(` + SELECT DATE(check_out) as day, COALESCE(SUM(total_amount), 0) as revenue + FROM reservations + WHERE check_out >= CURRENT_DATE - INTERVAL '7 days' AND status = 'checked_out' + GROUP BY DATE(check_out) ORDER BY day + `); + res.json({ weeklyRevenue: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener ingresos semanales' }); + } +}; + +const getTodayArrivals = async (req, res) => { + try { + const today = new Date().toISOString().split('T')[0]; + const result = await pool.query(` + SELECT r.id, r.check_in, r.check_out, r.room_id, r.status, + g.first_name, g.last_name, g.phone + FROM reservations r + JOIN guests g ON g.id = r.guest_id + WHERE (r.check_in = $1 OR r.check_out = $1) + AND r.status IN ('confirmed', 'checked_in') + ORDER BY r.check_in + `, [today]); + res.json({ arrivals: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener llegadas' }); + } +}; + +module.exports = { getKPIs, getWeeklyRevenue, getTodayArrivals }; diff --git a/backend/hotel_hacienda/src/controllers/rooms.controller.js b/backend/hotel_hacienda/src/controllers/rooms.controller.js new file mode 100644 index 0000000..ff04633 --- /dev/null +++ b/backend/hotel_hacienda/src/controllers/rooms.controller.js @@ -0,0 +1,62 @@ +const pool = require('../db/connection'); + +const getRoomsWithStatus = async (req, res) => { + try { + const result = await pool.query(` + SELECT r.*, + res.id as reservation_id, + g.first_name || ' ' || g.last_name as guest_name, + g.phone as guest_phone, + res.check_in, + res.check_out + FROM rooms r + LEFT JOIN reservations res ON res.room_id = r.id_room AND res.status = 'checked_in' + LEFT JOIN guests g ON g.id = res.guest_id + ORDER BY r.floor, r.name_room + `); + res.json({ rooms: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener estado de habitaciones' }); + } +}; + +const updateRoomStatus = async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body; + const userId = req.user?.user_id; + + const validStatuses = ['available', 'occupied', 'cleaning', 'maintenance']; + if (!validStatuses.includes(status)) { + return res.status(400).json({ message: 'Estado invalido' }); + } + + const current = await pool.query('SELECT status FROM rooms WHERE id_room = $1', [id]); + if (current.rows.length === 0) { + return res.status(404).json({ message: 'Habitacion no encontrada' }); + } + const previousStatus = current.rows[0].status; + + await pool.query('UPDATE rooms SET status = $1 WHERE id_room = $2', [status, id]); + + await pool.query( + 'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)', + [id, previousStatus, status, userId] + ); + + if (status === 'cleaning') { + await pool.query( + `INSERT INTO housekeeping_tasks (room_id, priority, type, status) VALUES ($1, 'high', 'checkout', 'pending')`, + [id] + ); + } + + res.json({ message: 'Estado actualizado correctamente' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al actualizar estado' }); + } +}; + +module.exports = { getRoomsWithStatus, updateRoomStatus }; diff --git a/backend/hotel_hacienda/src/routes/dashboard.routes.js b/backend/hotel_hacienda/src/routes/dashboard.routes.js new file mode 100644 index 0000000..91b2d54 --- /dev/null +++ b/backend/hotel_hacienda/src/routes/dashboard.routes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const dashboardController = require('../controllers/dashboard.controller'); + +router.get('/kpis', dashboardController.getKPIs); +router.get('/weekly-revenue', dashboardController.getWeeklyRevenue); +router.get('/today-arrivals', dashboardController.getTodayArrivals); + +module.exports = router; diff --git a/backend/hotel_hacienda/src/routes/rooms.routes.js b/backend/hotel_hacienda/src/routes/rooms.routes.js new file mode 100644 index 0000000..15f6d21 --- /dev/null +++ b/backend/hotel_hacienda/src/routes/rooms.routes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const roomsController = require('../controllers/rooms.controller'); + +router.get('/status', roomsController.getRoomsWithStatus); +router.put('/:id/status', roomsController.updateRoomStatus); + +module.exports = router; diff --git a/frontend/Frontend-Hotel/src/App.jsx b/frontend/Frontend-Hotel/src/App.jsx index 0587c53..e55cc78 100644 --- a/frontend/Frontend-Hotel/src/App.jsx +++ b/frontend/Frontend-Hotel/src/App.jsx @@ -39,6 +39,7 @@ import InventoryReport from "./pages/Inventory/InventoryReport.jsx"; 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 "./styles/global.css"; //SubmenĂș de Hotel @@ -131,6 +132,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/Frontend-Hotel/src/constants/menuconfig.js b/frontend/Frontend-Hotel/src/constants/menuconfig.js index 5a15565..ba71488 100644 --- a/frontend/Frontend-Hotel/src/constants/menuconfig.js +++ b/frontend/Frontend-Hotel/src/constants/menuconfig.js @@ -1,6 +1,16 @@ // src/constants/menuConfig.js export const menuConfig = { + operations: { + label: "Operations", + spanish_label: "Operaciones", + basePath: "/app/room-dashboard", + submenu: [ + { label: "Room Dashboard", spanish_label: "Panel de Habitaciones", route: "/app/room-dashboard" }, + { label: "Reservations", spanish_label: "Reservaciones", route: "/app/reservations" }, + { label: "Guests", spanish_label: "Huespedes", route: "/app/guests" }, + ], + }, dashboards: { label: "Dashboards", spanish_label: "Tableros", diff --git a/frontend/Frontend-Hotel/src/pages/RoomDashboard/RoomDashboard.jsx b/frontend/Frontend-Hotel/src/pages/RoomDashboard/RoomDashboard.jsx new file mode 100644 index 0000000..0ec20b9 --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/RoomDashboard/RoomDashboard.jsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import KPIBar from "./components/KPIBar"; +import RoomGrid from "./components/RoomGrid"; +import RoomDetailModal from "./components/RoomDetailModal"; +import { getRoomsWithStatus, getDashboardKPIs, updateRoomStatus } from "../../services/roomService"; + +export default function RoomDashboard() { + const { t } = useTranslation(); + const [rooms, setRooms] = useState([]); + const [kpis, setKpis] = useState({}); + const [selectedRoom, setSelectedRoom] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchData = async () => { + try { + const [roomsRes, kpisRes] = await Promise.all([ + getRoomsWithStatus(), + getDashboardKPIs(), + ]); + setRooms(roomsRes.data.rooms); + setKpis(kpisRes.data); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchData(); }, []); + + const handleStatusChange = async (roomId, newStatus) => { + await updateRoomStatus(roomId, newStatus); + fetchData(); + setSelectedRoom(null); + }; + + if (loading) return

{t('common.loading')}

; + + return ( +
+
+

{t('rooms.title')}

+
+ +
+ +
+ {selectedRoom && ( + setSelectedRoom(null)} + onStatusChange={handleStatusChange} + /> + )} +
+ ); +} diff --git a/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/KPIBar.jsx b/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/KPIBar.jsx new file mode 100644 index 0000000..681b27c --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/KPIBar.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +export default function KPIBar({ kpis }) { + const { t } = useTranslation(); + + return ( +
+
+ {kpis.occupancy ?? 0}% + {t("rooms.occupancyRate")} +
+
+ {kpis.availableRooms ?? 0} + {t("rooms.availableRooms")} +
+
+ {kpis.todayCheckIns ?? 0} + {t("rooms.todayCheckIns")} +
+
+ {kpis.todayCheckOuts ?? 0} + {t("rooms.todayCheckOuts")} +
+
+ ); +} diff --git a/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomDetailModal.jsx b/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomDetailModal.jsx new file mode 100644 index 0000000..142f3d4 --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomDetailModal.jsx @@ -0,0 +1,180 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +export default function RoomDetailModal({ room, onClose, onStatusChange }) { + const { t } = useTranslation(); + + if (!room) return null; + + const amenities = Array.isArray(room.amenities) ? room.amenities : []; + + return ( +
+
e.stopPropagation()}> +
+

+ {t("rooms.roomNumber")}: {room.name_room} +

+ +
+ +
+
+
+ {t("rooms.roomType")} +

+ {room.bed_type} +

+
+
+ {t("rooms.floor")} +

+ {room.floor} +

+
+
+ +
+ {t("common.status")} +
+ + {t(`rooms.${room.status || "available"}`)} + +
+
+ + {room.guest_name && ( +
+ + {t("rooms.guestInfo")} + +
+

+ {t("common.name")}: {room.guest_name} +

+ {room.guest_phone && ( +

+ {t("common.phone")}: {room.guest_phone} +

+ )} + {room.check_in && ( +

+ {t("reservations.checkIn")}:{" "} + {new Date(room.check_in).toLocaleDateString()} +

+ )} + {room.check_out && ( +

+ {t("reservations.checkOut")}:{" "} + {new Date(room.check_out).toLocaleDateString()} +

+ )} +
+
+ )} + + {amenities.length > 0 && ( +
+ {t("rooms.amenities")} +
+ {amenities.map((amenity, index) => ( + + {amenity} + + ))} +
+
+ )} + +
+ {t("rooms.pricePerNight")} +

+ ${room.cost_per_nigth} +

+
+ +
+ {room.status === "available" && ( + + )} + {room.status === "cleaning" && ( + + )} + {room.status === "maintenance" && ( + + )} +
+
+
+
+ ); +} diff --git a/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomGrid.jsx b/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomGrid.jsx new file mode 100644 index 0000000..aaea716 --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomGrid.jsx @@ -0,0 +1,73 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +export default function RoomGrid({ rooms, onRoomClick }) { + const { t } = useTranslation(); + + const roomsByFloor = rooms.reduce((acc, room) => { + const floor = room.floor ?? 1; + if (!acc[floor]) acc[floor] = []; + acc[floor].push(room); + return acc; + }, {}); + + const sortedFloors = Object.keys(roomsByFloor) + .map(Number) + .sort((a, b) => a - b); + + return ( +
+ {sortedFloors.map((floor) => ( +
+

+ {t("rooms.floor")} {floor} +

+
+ {roomsByFloor[floor].map((room) => ( +
onRoomClick(room)} + > +
{room.name_room}
+ {room.guest_name && ( +
{room.guest_name}
+ )} +
+ + {t(`rooms.${room.status || "available"}`)} + + + ${room.cost_per_nigth} + +
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/frontend/Frontend-Hotel/src/services/roomService.js b/frontend/Frontend-Hotel/src/services/roomService.js new file mode 100644 index 0000000..4335e8d --- /dev/null +++ b/frontend/Frontend-Hotel/src/services/roomService.js @@ -0,0 +1,7 @@ +import api from "./api"; + +export const getRoomsWithStatus = () => api.get("/rooms/status"); +export const updateRoomStatus = (id, status) => api.put(`/rooms/${id}/status`, { status }); +export const getDashboardKPIs = () => api.get("/dashboard/kpis"); +export const getWeeklyRevenue = () => api.get("/dashboard/weekly-revenue"); +export const getTodayArrivals = () => api.get("/dashboard/today-arrivals");