From 785b1f092520139acafdb11cbd610b25e7b67b26 Mon Sep 17 00:00:00 2001 From: ialcarazsalazar Date: Sun, 15 Feb 2026 01:41:24 +0000 Subject: [PATCH] feat: add guest management with profiles and stay history Co-Authored-By: Claude Opus 4.6 --- backend/hotel_hacienda/src/app.js | 2 + .../src/controllers/guests.controller.js | 108 ++++++ .../src/routes/guests.routes.js | 11 + frontend/Frontend-Hotel/src/App.jsx | 4 + .../src/pages/Guests/GuestDetail.jsx | 363 ++++++++++++++++++ .../src/pages/Guests/Guests.jsx | 277 +++++++++++++ .../src/services/guestService.js | 6 + 7 files changed, 771 insertions(+) create mode 100644 backend/hotel_hacienda/src/controllers/guests.controller.js create mode 100644 backend/hotel_hacienda/src/routes/guests.routes.js create mode 100644 frontend/Frontend-Hotel/src/pages/Guests/GuestDetail.jsx create mode 100644 frontend/Frontend-Hotel/src/pages/Guests/Guests.jsx create mode 100644 frontend/Frontend-Hotel/src/services/guestService.js diff --git a/backend/hotel_hacienda/src/app.js b/backend/hotel_hacienda/src/app.js index 84f0350..b14f357 100644 --- a/backend/hotel_hacienda/src/app.js +++ b/backend/hotel_hacienda/src/app.js @@ -37,6 +37,7 @@ const incomehrxRoutes = require('./routes/incomehrx.routes'); const roomsRoutes = require('./routes/rooms.routes'); const dashboardRoutes = require('./routes/dashboard.routes'); const reservationsRoutes = require('./routes/reservations.routes'); +const guestsRoutes = require('./routes/guests.routes'); //Prefijo - Auth routes are public (no middleware) app.use('/api/auth', authRoutes); @@ -60,5 +61,6 @@ app.use('/api/restaurantpl', authMiddleware, restaurantRoutes); app.use('/api/rooms', authMiddleware, roomsRoutes); app.use('/api/dashboard', authMiddleware, dashboardRoutes); app.use('/api/reservations', authMiddleware, reservationsRoutes); +app.use('/api/guests', authMiddleware, guestsRoutes); module.exports = app; diff --git a/backend/hotel_hacienda/src/controllers/guests.controller.js b/backend/hotel_hacienda/src/controllers/guests.controller.js new file mode 100644 index 0000000..61b6ca7 --- /dev/null +++ b/backend/hotel_hacienda/src/controllers/guests.controller.js @@ -0,0 +1,108 @@ +const pool = require('../db/connection'); + +const getGuests = async (req, res) => { + try { + const { search, limit = 50, offset = 0 } = req.query; + let query = ` + SELECT g.*, + r.room_id, rm.name_room, + r.check_in as current_check_in + FROM guests g + LEFT JOIN reservations r ON r.guest_id = g.id AND r.status = 'checked_in' + LEFT JOIN rooms rm ON rm.id_room = r.room_id + `; + const params = []; + let paramIndex = 1; + + if (search) { + query += ` WHERE g.first_name ILIKE $${paramIndex} OR g.last_name ILIKE $${paramIndex} OR g.email ILIKE $${paramIndex} OR g.phone ILIKE $${paramIndex}`; + params.push(`%${search}%`); + paramIndex++; + } + query += ` ORDER BY g.created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(parseInt(limit), parseInt(offset)); + + const result = await pool.query(query, params); + const countQuery = search + ? await pool.query('SELECT COUNT(*) FROM guests WHERE first_name ILIKE $1 OR last_name ILIKE $1 OR email ILIKE $1 OR phone ILIKE $1', [`%${search}%`]) + : await pool.query('SELECT COUNT(*) FROM guests'); + + res.json({ guests: result.rows, total: parseInt(countQuery.rows[0].count) }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener huespedes' }); + } +}; + +const getGuestById = async (req, res) => { + try { + const { id } = req.params; + const guest = await pool.query(` + SELECT g.*, r.room_id, rm.name_room, r.check_in as current_check_in, r.check_out as current_check_out + FROM guests g + LEFT JOIN reservations r ON r.guest_id = g.id AND r.status = 'checked_in' + LEFT JOIN rooms rm ON rm.id_room = r.room_id + WHERE g.id = $1 + `, [id]); + + if (guest.rows.length === 0) return res.status(404).json({ message: 'Huesped no encontrado' }); + res.json({ guest: guest.rows[0] }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener huesped' }); + } +}; + +const createGuest = async (req, res) => { + try { + const { first_name, last_name, email, phone, id_type, id_number, nationality, address, notes } = req.body; + const result = await pool.query( + `INSERT INTO guests (first_name, last_name, email, phone, id_type, id_number, nationality, address, notes) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, + [first_name, last_name, email, phone, id_type, id_number, nationality, address, notes] + ); + res.status(201).json({ guest: result.rows[0], message: 'Huesped creado correctamente' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al crear huesped' }); + } +}; + +const updateGuest = async (req, res) => { + try { + const { id } = req.params; + const { first_name, last_name, email, phone, id_type, id_number, nationality, address, notes } = req.body; + const result = await pool.query( + `UPDATE guests SET first_name = COALESCE($1, first_name), last_name = COALESCE($2, last_name), + email = COALESCE($3, email), phone = COALESCE($4, phone), id_type = COALESCE($5, id_type), + id_number = COALESCE($6, id_number), nationality = COALESCE($7, nationality), + address = COALESCE($8, address), notes = COALESCE($9, notes), updated_at = NOW() + WHERE id = $10 RETURNING *`, + [first_name, last_name, email, phone, id_type, id_number, nationality, address, notes, id] + ); + if (result.rows.length === 0) return res.status(404).json({ message: 'Huesped no encontrado' }); + res.json({ guest: result.rows[0] }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al actualizar huesped' }); + } +}; + +const getGuestStays = async (req, res) => { + try { + const { id } = req.params; + const result = await pool.query(` + SELECT gs.*, rm.name_room, rm.bed_type + FROM guest_stays gs + LEFT JOIN rooms rm ON rm.id_room = gs.room_id + WHERE gs.guest_id = $1 + ORDER BY gs.check_in DESC + `, [id]); + res.json({ stays: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener estadias' }); + } +}; + +module.exports = { getGuests, getGuestById, createGuest, updateGuest, getGuestStays }; diff --git a/backend/hotel_hacienda/src/routes/guests.routes.js b/backend/hotel_hacienda/src/routes/guests.routes.js new file mode 100644 index 0000000..be3768f --- /dev/null +++ b/backend/hotel_hacienda/src/routes/guests.routes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('../controllers/guests.controller'); + +router.get('/', ctrl.getGuests); +router.get('/:id', ctrl.getGuestById); +router.get('/:id/stays', ctrl.getGuestStays); +router.post('/', ctrl.createGuest); +router.put('/:id', ctrl.updateGuest); + +module.exports = router; diff --git a/frontend/Frontend-Hotel/src/App.jsx b/frontend/Frontend-Hotel/src/App.jsx index f75cabb..68dfb3d 100644 --- a/frontend/Frontend-Hotel/src/App.jsx +++ b/frontend/Frontend-Hotel/src/App.jsx @@ -42,6 +42,8 @@ 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 Guests from "./pages/Guests/Guests.jsx"; +import GuestDetail from "./pages/Guests/GuestDetail.jsx"; import "./styles/global.css"; //SubmenĂș de Hotel @@ -137,6 +139,8 @@ export default function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/Frontend-Hotel/src/pages/Guests/GuestDetail.jsx b/frontend/Frontend-Hotel/src/pages/Guests/GuestDetail.jsx new file mode 100644 index 0000000..76b16ff --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/Guests/GuestDetail.jsx @@ -0,0 +1,363 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams, useNavigate } from "react-router-dom"; +import { getGuestById, updateGuest, getGuestStays } from "../../services/guestService"; + +export default function GuestDetail() { + const { t } = useTranslation(); + const { id } = useParams(); + const navigate = useNavigate(); + const [guest, setGuest] = useState(null); + const [stays, setStays] = useState([]); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const [form, setForm] = useState({}); + + const fetchGuest = async () => { + try { + setLoading(true); + const [guestRes, staysRes] = await Promise.all([ + getGuestById(id), + getGuestStays(id), + ]); + setGuest(guestRes.data.guest); + setStays(staysRes.data.stays || []); + setForm(guestRes.data.guest); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchGuest(); + }, [id]); + + const handleUpdate = async (e) => { + e.preventDefault(); + try { + await updateGuest(id, form); + setEditing(false); + fetchGuest(); + } catch (error) { + console.error(error); + } + }; + + const getInitials = (firstName, lastName) => { + const f = firstName ? firstName.charAt(0).toUpperCase() : ""; + const l = lastName ? lastName.charAt(0).toUpperCase() : ""; + return f + l; + }; + + const formatDate = (dateStr) => { + if (!dateStr) return "-"; + return new Date(dateStr).toLocaleDateString(); + }; + + const renderStars = (rating) => { + if (!rating) return "-"; + const stars = []; + for (let i = 1; i <= 5; i++) { + stars.push( + + ★ + + ); + } + return stars; + }; + + if (loading) { + return ( +
+

{t("common.loading")}

+
+ ); + } + + if (!guest) { + return ( +
+

{t("common.noResults")}

+
+ ); + } + + return ( +
+
+ +
+ + {/* Current Room Banner */} + {guest.name_room && ( +
+
+ + {t("guests.currentRoom")} + +

{guest.name_room}

+
+
+ + {t("reservations.checkIn")}: {formatDate(guest.current_check_in)} + + {guest.current_check_out && ( + + {t("reservations.checkOut")}: {formatDate(guest.current_check_out)} + + )} +
+
+ )} + +
+ {/* Profile Section */} +
+ {!editing ? ( +
+
+
+
+ {getInitials(guest.first_name, guest.last_name)} +
+
+

+ {guest.first_name} {guest.last_name} +

+ {guest.nationality && ( + + {guest.nationality} + + )} +
+
+ +
+ +
+ {guest.email && ( +
+ {t("guests.email")} +

{guest.email}

+
+ )} + {guest.phone && ( +
+ {t("guests.phone")} +

{guest.phone}

+
+ )} + {guest.id_type && ( +
+
+ {t("guests.idType")} +

{guest.id_type}

+
+
+ {t("guests.idNumber")} +

{guest.id_number}

+
+
+ )} + {guest.address && ( +
+ {t("guests.address")} +

{guest.address}

+
+ )} + {guest.notes && ( +
+ {t("common.notes")} +

{guest.notes}

+
+ )} +
+
+ ) : ( +
+

{t("common.edit")}

+
+
+ + setForm({ ...form, first_name: e.target.value })} + /> +
+
+ + setForm({ ...form, last_name: e.target.value })} + /> +
+
+
+
+ + setForm({ ...form, email: e.target.value })} + /> +
+
+ + setForm({ ...form, phone: e.target.value })} + /> +
+
+
+
+ + setForm({ ...form, id_type: e.target.value })} + /> +
+
+ + setForm({ ...form, id_number: e.target.value })} + /> +
+
+
+ + setForm({ ...form, nationality: e.target.value })} + /> +
+
+ + setForm({ ...form, address: e.target.value })} + /> +
+
+ +