diff --git a/backend/hotel_hacienda/src/app.js b/backend/hotel_hacienda/src/app.js
index b14f357..9a0005e 100644
--- a/backend/hotel_hacienda/src/app.js
+++ b/backend/hotel_hacienda/src/app.js
@@ -38,6 +38,9 @@ const roomsRoutes = require('./routes/rooms.routes');
const dashboardRoutes = require('./routes/dashboard.routes');
const reservationsRoutes = require('./routes/reservations.routes');
const guestsRoutes = require('./routes/guests.routes');
+const housekeepingRoutes = require('./routes/housekeeping.routes');
+const roomserviceRoutes = require('./routes/roomservice.routes');
+const eventsRoutes = require('./routes/events.routes');
//Prefijo - Auth routes are public (no middleware)
app.use('/api/auth', authRoutes);
@@ -62,5 +65,8 @@ app.use('/api/rooms', authMiddleware, roomsRoutes);
app.use('/api/dashboard', authMiddleware, dashboardRoutes);
app.use('/api/reservations', authMiddleware, reservationsRoutes);
app.use('/api/guests', authMiddleware, guestsRoutes);
+app.use('/api/housekeeping', authMiddleware, housekeepingRoutes);
+app.use('/api/room-service', authMiddleware, roomserviceRoutes);
+app.use('/api/events', authMiddleware, eventsRoutes);
module.exports = app;
diff --git a/backend/hotel_hacienda/src/controllers/events.controller.js b/backend/hotel_hacienda/src/controllers/events.controller.js
new file mode 100644
index 0000000..e637609
--- /dev/null
+++ b/backend/hotel_hacienda/src/controllers/events.controller.js
@@ -0,0 +1,111 @@
+const pool = require('../db/connection');
+
+const getVenues = async (req, res) => {
+ try {
+ const result = await pool.query('SELECT * FROM venues ORDER BY name');
+ res.json({ venues: result.rows });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al obtener salones' });
+ }
+};
+
+const createVenue = async (req, res) => {
+ try {
+ const { name, capacity, area_sqm, price_per_hour, amenities, description } = req.body;
+ const result = await pool.query(
+ 'INSERT INTO venues (name, capacity, area_sqm, price_per_hour, amenities, description) VALUES ($1, $2, $3, $4, $5::jsonb, $6) RETURNING *',
+ [name, capacity, area_sqm, price_per_hour, JSON.stringify(amenities || []), description]
+ );
+ res.status(201).json({ venue: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al crear salon' });
+ }
+};
+
+const updateVenue = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { name, capacity, area_sqm, price_per_hour, amenities, description, status } = req.body;
+ const result = await pool.query(
+ `UPDATE venues SET name = COALESCE($1, name), capacity = COALESCE($2, capacity),
+ area_sqm = COALESCE($3, area_sqm), price_per_hour = COALESCE($4, price_per_hour),
+ amenities = COALESCE($5::jsonb, amenities), description = COALESCE($6, description),
+ status = COALESCE($7, status)
+ WHERE id = $8 RETURNING *`,
+ [name, capacity, area_sqm, price_per_hour, amenities ? JSON.stringify(amenities) : null, description, status, id]
+ );
+ if (result.rows.length === 0) return res.status(404).json({ message: 'Salon no encontrado' });
+ res.json({ venue: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar salon' });
+ }
+};
+
+const getEvents = async (req, res) => {
+ try {
+ const { from_date, to_date } = req.query;
+ let query = 'SELECT e.*, v.name as venue_name, v.capacity FROM events e JOIN venues v ON v.id = e.venue_id WHERE 1=1';
+ const params = [];
+ let idx = 1;
+ if (from_date) { query += ` AND e.event_date >= $${idx++}`; params.push(from_date); }
+ if (to_date) { query += ` AND e.event_date <= $${idx++}`; params.push(to_date); }
+ query += ' ORDER BY e.event_date, e.start_time';
+ const result = await pool.query(query, params);
+ res.json({ events: result.rows });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al obtener eventos' });
+ }
+};
+
+const createEvent = async (req, res) => {
+ try {
+ const { venue_id, name, organizer, event_date, start_time, end_time, guest_count, notes, total_amount } = req.body;
+
+ // Check venue availability
+ const overlap = await pool.query(
+ `SELECT id FROM events WHERE venue_id = $1 AND event_date = $2
+ AND ((start_time < $4 AND end_time > $3)) AND status != 'cancelled'`,
+ [venue_id, event_date, start_time, end_time]
+ );
+ if (overlap.rows.length > 0) {
+ return res.status(409).json({ message: 'El salon no esta disponible en ese horario' });
+ }
+
+ const result = await pool.query(
+ `INSERT INTO events (venue_id, name, organizer, event_date, start_time, end_time, guest_count, notes, total_amount)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`,
+ [venue_id, name, organizer, event_date, start_time, end_time, guest_count, notes, total_amount]
+ );
+ res.status(201).json({ event: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al crear evento' });
+ }
+};
+
+const updateEvent = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { venue_id, name, organizer, event_date, start_time, end_time, guest_count, notes, total_amount, status } = req.body;
+ const result = await pool.query(
+ `UPDATE events SET venue_id = COALESCE($1, venue_id), name = COALESCE($2, name),
+ organizer = COALESCE($3, organizer), event_date = COALESCE($4, event_date),
+ start_time = COALESCE($5, start_time), end_time = COALESCE($6, end_time),
+ guest_count = COALESCE($7, guest_count), notes = COALESCE($8, notes),
+ total_amount = COALESCE($9, total_amount), status = COALESCE($10, status)
+ WHERE id = $11 RETURNING *`,
+ [venue_id, name, organizer, event_date, start_time, end_time, guest_count, notes, total_amount, status, id]
+ );
+ if (result.rows.length === 0) return res.status(404).json({ message: 'Evento no encontrado' });
+ res.json({ event: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar evento' });
+ }
+};
+
+module.exports = { getVenues, createVenue, updateVenue, getEvents, createEvent, updateEvent };
diff --git a/backend/hotel_hacienda/src/controllers/housekeeping.controller.js b/backend/hotel_hacienda/src/controllers/housekeeping.controller.js
new file mode 100644
index 0000000..1408c9a
--- /dev/null
+++ b/backend/hotel_hacienda/src/controllers/housekeeping.controller.js
@@ -0,0 +1,109 @@
+const pool = require('../db/connection');
+
+const getTasks = async (req, res) => {
+ try {
+ const { status, priority, assigned_to } = req.query;
+ let query = `
+ SELECT ht.*, rm.name_room, rm.floor,
+ CASE WHEN ht.assigned_to IS NOT NULL THEN
+ (SELECT e.first_name || ' ' || e.last_name FROM employees e WHERE e.id_employee = ht.assigned_to)
+ END as assigned_name
+ FROM housekeeping_tasks ht
+ LEFT JOIN rooms rm ON rm.id_room = ht.room_id
+ WHERE 1=1
+ `;
+ const params = [];
+ let idx = 1;
+ if (status) { query += ` AND ht.status = $${idx++}`; params.push(status); }
+ if (priority) { query += ` AND ht.priority = $${idx++}`; params.push(priority); }
+ if (assigned_to) { query += ` AND ht.assigned_to = $${idx++}`; params.push(assigned_to); }
+ query += ' ORDER BY CASE ht.priority WHEN \'high\' THEN 1 WHEN \'normal\' THEN 2 WHEN \'low\' THEN 3 END, ht.created_at DESC';
+
+ const result = await pool.query(query, params);
+ res.json({ tasks: result.rows });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al obtener tareas' });
+ }
+};
+
+const createTask = async (req, res) => {
+ try {
+ const { room_id, priority, type, notes } = req.body;
+ const result = await pool.query(
+ 'INSERT INTO housekeeping_tasks (room_id, priority, type, notes) VALUES ($1, $2, $3, $4) RETURNING *',
+ [room_id, priority || 'normal', type, notes]
+ );
+ res.status(201).json({ task: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al crear tarea' });
+ }
+};
+
+const updateTask = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { assigned_to, status, notes } = req.body;
+
+ const current = await pool.query('SELECT * FROM housekeeping_tasks WHERE id = $1', [id]);
+ if (current.rows.length === 0) return res.status(404).json({ message: 'Tarea no encontrada' });
+
+ const task = current.rows[0];
+ const updates = {};
+
+ if (assigned_to !== undefined) updates.assigned_to = assigned_to;
+ if (notes !== undefined) updates.notes = notes;
+
+ if (status === 'in_progress' && task.status === 'pending') {
+ updates.status = 'in_progress';
+ updates.started_at = new Date();
+ } else if (status === 'completed' && task.status === 'in_progress') {
+ updates.status = 'completed';
+ updates.completed_at = new Date();
+ // Set room to available
+ await pool.query("UPDATE rooms SET status = 'available' WHERE id_room = $1", [task.room_id]);
+ await pool.query(
+ 'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',
+ [task.room_id, 'cleaning', 'available', req.user?.user_id]
+ );
+ } else if (status) {
+ updates.status = status;
+ }
+
+ const setClauses = Object.keys(updates).map((key, i) => `${key} = $${i + 1}`);
+ const values = Object.values(updates);
+ if (setClauses.length === 0) return res.json({ task: task });
+
+ const result = await pool.query(
+ `UPDATE housekeeping_tasks SET ${setClauses.join(', ')} WHERE id = $${values.length + 1} RETURNING *`,
+ [...values, id]
+ );
+ res.json({ task: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar tarea' });
+ }
+};
+
+const getStaff = async (req, res) => {
+ try {
+ // Get employees from housekeeping-related areas
+ // Note: the employees table structure uses stored functions, so we query directly
+ const result = await pool.query(`
+ SELECT e.id_employee, e.first_name, e.last_name,
+ (SELECT COUNT(*) FROM housekeeping_tasks ht WHERE ht.assigned_to = e.id_employee AND ht.status = 'in_progress') as active_tasks
+ FROM employees e
+ WHERE e.status_employee = true
+ ORDER BY e.first_name
+ LIMIT 50
+ `);
+ res.json({ staff: result.rows });
+ } catch (error) {
+ console.error(error);
+ // If employee table structure is different, return empty
+ res.json({ staff: [] });
+ }
+};
+
+module.exports = { getTasks, createTask, updateTask, getStaff };
diff --git a/backend/hotel_hacienda/src/controllers/roomservice.controller.js b/backend/hotel_hacienda/src/controllers/roomservice.controller.js
new file mode 100644
index 0000000..05de0c1
--- /dev/null
+++ b/backend/hotel_hacienda/src/controllers/roomservice.controller.js
@@ -0,0 +1,130 @@
+const pool = require('../db/connection');
+
+const getOrders = async (req, res) => {
+ try {
+ const { status } = req.query;
+ let query = `
+ SELECT rso.*, rm.name_room, g.first_name || ' ' || g.last_name as guest_name,
+ json_agg(json_build_object('id', oi.id, 'name', mi.name, 'quantity', oi.quantity, 'price', oi.price, 'notes', oi.notes)) as items
+ FROM room_service_orders rso
+ LEFT JOIN rooms rm ON rm.id_room = rso.room_id
+ LEFT JOIN guests g ON g.id = rso.guest_id
+ LEFT JOIN order_items oi ON oi.order_id = rso.id
+ LEFT JOIN menu_items mi ON mi.id = oi.menu_item_id
+ `;
+ const params = [];
+ if (status) {
+ query += ' WHERE rso.status = $1';
+ params.push(status);
+ } else {
+ query += " WHERE rso.status NOT IN ('delivered', 'cancelled')";
+ }
+ query += ' GROUP BY rso.id, rm.name_room, g.first_name, g.last_name ORDER BY rso.created_at DESC';
+
+ const result = await pool.query(query, params);
+ res.json({ orders: result.rows });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al obtener ordenes' });
+ }
+};
+
+const createOrder = async (req, res) => {
+ try {
+ const { room_id, guest_id, items, notes } = req.body;
+
+ // Calculate total
+ let total = 0;
+ for (const item of items) {
+ const menuItem = await pool.query('SELECT price FROM menu_items WHERE id = $1', [item.menu_item_id]);
+ if (menuItem.rows.length > 0) {
+ total += parseFloat(menuItem.rows[0].price) * item.quantity;
+ }
+ }
+
+ const orderResult = await pool.query(
+ 'INSERT INTO room_service_orders (room_id, guest_id, total, notes) VALUES ($1, $2, $3, $4) RETURNING *',
+ [room_id, guest_id || null, total, notes]
+ );
+ const orderId = orderResult.rows[0].id;
+
+ // Insert order items
+ for (const item of items) {
+ const menuItem = await pool.query('SELECT price FROM menu_items WHERE id = $1', [item.menu_item_id]);
+ const price = menuItem.rows.length > 0 ? menuItem.rows[0].price : 0;
+ await pool.query(
+ 'INSERT INTO order_items (order_id, menu_item_id, quantity, price, notes) VALUES ($1, $2, $3, $4, $5)',
+ [orderId, item.menu_item_id, item.quantity, price, item.notes || null]
+ );
+ }
+
+ res.status(201).json({ order: orderResult.rows[0], message: 'Orden creada correctamente' });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al crear orden' });
+ }
+};
+
+const updateOrderStatus = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { status } = req.body;
+ const validStatuses = ['pending', 'preparing', 'delivering', 'delivered', 'cancelled'];
+ if (!validStatuses.includes(status)) return res.status(400).json({ message: 'Estado invalido' });
+
+ const result = await pool.query(
+ 'UPDATE room_service_orders SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
+ [status, id]
+ );
+ if (result.rows.length === 0) return res.status(404).json({ message: 'Orden no encontrada' });
+ res.json({ order: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar orden' });
+ }
+};
+
+const getMenu = async (req, res) => {
+ try {
+ const result = await pool.query('SELECT * FROM menu_items ORDER BY category, name');
+ res.json({ menu: result.rows });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al obtener menu' });
+ }
+};
+
+const createMenuItem = async (req, res) => {
+ try {
+ const { name, name_es, description, description_es, price, category } = req.body;
+ const result = await pool.query(
+ 'INSERT INTO menu_items (name, name_es, description, description_es, price, category) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
+ [name, name_es, description, description_es, price, category]
+ );
+ res.status(201).json({ item: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al crear platillo' });
+ }
+};
+
+const updateMenuItem = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { name, name_es, description, description_es, price, category, available } = req.body;
+ const result = await pool.query(
+ `UPDATE menu_items SET name = COALESCE($1, name), name_es = COALESCE($2, name_es),
+ description = COALESCE($3, description), description_es = COALESCE($4, description_es),
+ price = COALESCE($5, price), category = COALESCE($6, category), available = COALESCE($7, available)
+ WHERE id = $8 RETURNING *`,
+ [name, name_es, description, description_es, price, category, available, id]
+ );
+ if (result.rows.length === 0) return res.status(404).json({ message: 'Platillo no encontrado' });
+ res.json({ item: result.rows[0] });
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ message: 'Error al actualizar platillo' });
+ }
+};
+
+module.exports = { getOrders, createOrder, updateOrderStatus, getMenu, createMenuItem, updateMenuItem };
diff --git a/backend/hotel_hacienda/src/routes/events.routes.js b/backend/hotel_hacienda/src/routes/events.routes.js
new file mode 100644
index 0000000..2c5d877
--- /dev/null
+++ b/backend/hotel_hacienda/src/routes/events.routes.js
@@ -0,0 +1,12 @@
+const express = require('express');
+const router = express.Router();
+const ctrl = require('../controllers/events.controller');
+
+router.get('/venues', ctrl.getVenues);
+router.post('/venues', ctrl.createVenue);
+router.put('/venues/:id', ctrl.updateVenue);
+router.get('/', ctrl.getEvents);
+router.post('/', ctrl.createEvent);
+router.put('/:id', ctrl.updateEvent);
+
+module.exports = router;
diff --git a/backend/hotel_hacienda/src/routes/housekeeping.routes.js b/backend/hotel_hacienda/src/routes/housekeeping.routes.js
new file mode 100644
index 0000000..403070b
--- /dev/null
+++ b/backend/hotel_hacienda/src/routes/housekeeping.routes.js
@@ -0,0 +1,10 @@
+const express = require('express');
+const router = express.Router();
+const ctrl = require('../controllers/housekeeping.controller');
+
+router.get('/tasks', ctrl.getTasks);
+router.get('/staff', ctrl.getStaff);
+router.post('/tasks', ctrl.createTask);
+router.put('/tasks/:id', ctrl.updateTask);
+
+module.exports = router;
diff --git a/backend/hotel_hacienda/src/routes/roomservice.routes.js b/backend/hotel_hacienda/src/routes/roomservice.routes.js
new file mode 100644
index 0000000..e9d6e41
--- /dev/null
+++ b/backend/hotel_hacienda/src/routes/roomservice.routes.js
@@ -0,0 +1,12 @@
+const express = require('express');
+const router = express.Router();
+const ctrl = require('../controllers/roomservice.controller');
+
+router.get('/orders', ctrl.getOrders);
+router.post('/orders', ctrl.createOrder);
+router.put('/orders/:id/status', ctrl.updateOrderStatus);
+router.get('/menu', ctrl.getMenu);
+router.post('/menu', ctrl.createMenuItem);
+router.put('/menu/:id', ctrl.updateMenuItem);
+
+module.exports = router;
diff --git a/frontend/Frontend-Hotel/src/App.jsx b/frontend/Frontend-Hotel/src/App.jsx
index 68dfb3d..7734b70 100644
--- a/frontend/Frontend-Hotel/src/App.jsx
+++ b/frontend/Frontend-Hotel/src/App.jsx
@@ -44,6 +44,11 @@ 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 Housekeeping from "./pages/Housekeeping/Housekeeping.jsx";
+import RoomServiceOrders from "./pages/RoomService/RoomServiceOrders.jsx";
+import MenuManager from "./pages/RoomService/MenuManager.jsx";
+import NewOrder from "./pages/RoomService/NewOrder.jsx";
+import EventsVenues from "./pages/Events/EventsVenues.jsx";
import "./styles/global.css";
//SubmenĂș de Hotel
@@ -141,6 +146,11 @@ export default function App() {
} />
} />
} />
+ } />
+ } />
+ } />
+ } />
+ } />
>
diff --git a/frontend/Frontend-Hotel/src/constants/menuconfig.js b/frontend/Frontend-Hotel/src/constants/menuconfig.js
index ba71488..4dd6a8d 100644
--- a/frontend/Frontend-Hotel/src/constants/menuconfig.js
+++ b/frontend/Frontend-Hotel/src/constants/menuconfig.js
@@ -91,6 +91,16 @@ export const menuConfig = {
{ label: "Contracts", spanish_label: "Contratos", route: "/app/payroll/contract" }
],
},
+ services: {
+ label: "Services",
+ spanish_label: "Servicios",
+ basePath: "/app/housekeeping",
+ submenu: [
+ { label: "Housekeeping", spanish_label: "Limpieza", route: "/app/housekeeping" },
+ { label: "Room Service", spanish_label: "Servicio a Habitacion", route: "/app/room-service" },
+ { label: "Events & Venues", spanish_label: "Eventos y Salones", route: "/app/events" },
+ ],
+ },
// hotel: {
// label: "Hotel",
// spanish_label: "Hotel",
diff --git a/frontend/Frontend-Hotel/src/pages/Events/EventsVenues.jsx b/frontend/Frontend-Hotel/src/pages/Events/EventsVenues.jsx
new file mode 100644
index 0000000..4efb0a3
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/pages/Events/EventsVenues.jsx
@@ -0,0 +1,761 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ getVenues,
+ createVenue,
+ getEvents,
+ createEvent,
+} from "../../services/eventService";
+
+export default function EventsVenues() {
+ const { t } = useTranslation();
+ const [venues, setVenues] = useState([]);
+ const [events, setEvents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showVenueModal, setShowVenueModal] = useState(false);
+ const [showEventModal, setShowEventModal] = useState(false);
+
+ const [venueForm, setVenueForm] = useState({
+ name: "",
+ capacity: "",
+ area_sqm: "",
+ price_per_hour: "",
+ amenities: "",
+ description: "",
+ });
+
+ const [eventForm, setEventForm] = useState({
+ venue_id: "",
+ name: "",
+ organizer: "",
+ event_date: "",
+ start_time: "",
+ end_time: "",
+ guest_count: "",
+ notes: "",
+ total_amount: "",
+ });
+
+ const fetchData = async () => {
+ try {
+ const [venuesRes, eventsRes] = await Promise.all([
+ getVenues(),
+ getEvents(),
+ ]);
+ setVenues(venuesRes.data.venues || []);
+ setEvents(eventsRes.data.events || []);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const handleCreateVenue = async (e) => {
+ e.preventDefault();
+ try {
+ const amenitiesList = venueForm.amenities
+ ? venueForm.amenities.split(",").map((a) => a.trim()).filter(Boolean)
+ : [];
+ await createVenue({
+ ...venueForm,
+ capacity: parseInt(venueForm.capacity),
+ area_sqm: parseFloat(venueForm.area_sqm) || null,
+ price_per_hour: parseFloat(venueForm.price_per_hour),
+ amenities: amenitiesList,
+ });
+ setShowVenueModal(false);
+ setVenueForm({
+ name: "",
+ capacity: "",
+ area_sqm: "",
+ price_per_hour: "",
+ amenities: "",
+ description: "",
+ });
+ fetchData();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const handleCreateEvent = async (e) => {
+ e.preventDefault();
+ try {
+ await createEvent({
+ ...eventForm,
+ venue_id: parseInt(eventForm.venue_id),
+ guest_count: parseInt(eventForm.guest_count) || null,
+ total_amount: parseFloat(eventForm.total_amount) || null,
+ });
+ setShowEventModal(false);
+ setEventForm({
+ venue_id: "",
+ name: "",
+ organizer: "",
+ event_date: "",
+ start_time: "",
+ end_time: "",
+ guest_count: "",
+ notes: "",
+ total_amount: "",
+ });
+ fetchData();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const formatDate = (dateStr) => {
+ if (!dateStr) return "";
+ return new Date(dateStr).toLocaleDateString();
+ };
+
+ const formatTime = (timeStr) => {
+ if (!timeStr) return "";
+ return timeStr.substring(0, 5);
+ };
+
+ const getStatusBadge = (status) => {
+ switch (status) {
+ case "available":
+ return "badge badge-success";
+ case "reserved":
+ return "badge badge-warning";
+ default:
+ return "badge";
+ }
+ };
+
+ const getEventStatusBadge = (status) => {
+ switch (status) {
+ case "confirmed":
+ return "badge badge-success";
+ case "pending":
+ return "badge badge-warning";
+ case "cancelled":
+ return "badge badge-error";
+ case "completed":
+ return "badge badge-info";
+ default:
+ return "badge";
+ }
+ };
+
+ if (loading) {
+ return (
+
+
{t("common.loading")}
+
+ );
+ }
+
+ return (
+
+ {/* Venues Section */}
+
+
{t("events.title")}
+
+
+
+
+
+
+ {/* Venues */}
+
+ {t("events.venues")}
+
+
+ {venues.length === 0 ? (
+
+
{t("common.noResults")}
+
+ ) : (
+
+ {venues.map((venue) => (
+
+
+
+ {venue.name}
+
+
+ {t(`events.venueStatus.${venue.status || "available"}`)}
+
+
+
+
+
+
+ {t("events.capacity")}
+
+
+ {venue.capacity}
+
+
+
+
+ {t("events.area")}
+
+
+ {venue.area_sqm || "-"}
+
+
+
+
+
+
+ {t("events.pricePerHour")}
+
+
+ ${parseFloat(venue.price_per_hour || 0).toFixed(2)}
+
+
+
+ {venue.amenities && Array.isArray(venue.amenities) && venue.amenities.length > 0 && (
+
+ {venue.amenities.map((amenity, idx) => (
+
+ {amenity}
+
+ ))}
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* Upcoming Events */}
+
+ {t("events.upcomingEvents")}
+
+
+ {events.length === 0 ? (
+
+
{t("common.noResults")}
+
+ ) : (
+
+ {events.map((event) => (
+
+
+
+ {event.name}
+
+
+ {event.status}
+
+
+
+
+ {event.venue_name}
+
+
+
+
+
+ {t("events.eventDate")}
+
+
+ {formatDate(event.event_date)}
+
+
+
+
+ {t("events.startTime")} - {t("events.endTime")}
+
+
+ {formatTime(event.start_time)} - {formatTime(event.end_time)}
+
+
+
+
+
+
+
+ {t("events.organizer")}
+
+
+ {event.organizer}
+
+
+
+
+ {t("events.guestCount")}
+
+
+ {event.guest_count || "-"}
+
+
+
+
+ {event.total_amount && (
+
+
+ ${parseFloat(event.total_amount).toFixed(2)}
+
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* Add Venue Modal */}
+ {showVenueModal && (
+
setShowVenueModal(false)}>
+
e.stopPropagation()}
+ style={{ maxWidth: "550px" }}
+ >
+
{t("events.newVenue")}
+
+
+
+ )}
+
+ {/* New Event Modal */}
+ {showEventModal && (
+
setShowEventModal(false)}>
+
e.stopPropagation()}
+ style={{ maxWidth: "550px" }}
+ >
+
{t("events.newEvent")}
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/Frontend-Hotel/src/pages/Housekeeping/Housekeeping.jsx b/frontend/Frontend-Hotel/src/pages/Housekeeping/Housekeeping.jsx
new file mode 100644
index 0000000..48987d4
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/pages/Housekeeping/Housekeeping.jsx
@@ -0,0 +1,370 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ getHousekeepingTasks,
+ updateHousekeepingTask,
+ getHousekeepingStaff,
+} from "../../services/housekeepingService";
+
+export default function Housekeeping() {
+ const { t } = useTranslation();
+ const [tasks, setTasks] = useState([]);
+ const [staff, setStaff] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const fetchData = async () => {
+ try {
+ const [tasksRes, staffRes] = await Promise.all([
+ getHousekeepingTasks(),
+ getHousekeepingStaff(),
+ ]);
+ setTasks(tasksRes.data.tasks || []);
+ setStaff(staffRes.data.staff || []);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const pendingTasks = tasks.filter((t) => t.status === "pending");
+ const inProgressTasks = tasks.filter((t) => t.status === "in_progress");
+
+ const handleAssign = async (taskId, employeeId) => {
+ try {
+ await updateHousekeepingTask(taskId, { assigned_to: employeeId });
+ fetchData();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const handleStart = async (taskId) => {
+ try {
+ await updateHousekeepingTask(taskId, { status: "in_progress" });
+ fetchData();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const handleComplete = async (taskId) => {
+ try {
+ await updateHousekeepingTask(taskId, { status: "completed" });
+ fetchData();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const getTimeSince = (dateStr) => {
+ if (!dateStr) return "";
+ const diff = Date.now() - new Date(dateStr).getTime();
+ const minutes = Math.floor(diff / 60000);
+ if (minutes < 60) return `${minutes}m`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h`;
+ return `${Math.floor(hours / 24)}d`;
+ };
+
+ const getPriorityColor = (priority) => {
+ switch (priority) {
+ case "high":
+ return "var(--status-error, #ef4444)";
+ case "normal":
+ return "var(--accent-gold, #d4a853)";
+ case "low":
+ return "var(--text-muted, #6b7280)";
+ default:
+ return "var(--text-muted, #6b7280)";
+ }
+ };
+
+ const getTypeBadgeClass = (type) => {
+ switch (type) {
+ case "checkout":
+ return "badge badge-info";
+ case "maintenance":
+ return "badge badge-warning";
+ case "deep_clean":
+ return "badge badge-error";
+ default:
+ return "badge";
+ }
+ };
+
+ const getTypeLabel = (type) => {
+ const typeMap = {
+ checkout: t("housekeeping.type.checkout"),
+ maintenance: t("housekeeping.type.maintenance"),
+ deep_clean: t("housekeeping.type.deepClean"),
+ turndown: t("housekeeping.type.turndown"),
+ };
+ return typeMap[type] || type;
+ };
+
+ if (loading) {
+ return (
+
+
{t("common.loading")}
+
+ );
+ }
+
+ const TaskCard = ({ task, actions }) => (
+
+
+
+
+
+ {task.name_room || `Room ${task.room_id}`}
+
+ {task.floor && (
+
+ - {t("rooms.floor")} {task.floor}
+
+ )}
+
+
+ {getTimeSince(task.created_at)}
+
+
+
+
+ {getTypeLabel(task.type)}
+
+ {t(`housekeeping.priority.${task.priority || "normal"}`)}
+
+
+
+ {task.assigned_name && (
+
+ {t("housekeeping.assignTo")}: {task.assigned_name}
+
+ )}
+
+ {task.notes && (
+
+ {task.notes}
+
+ )}
+
+
+ {actions}
+
+
+ );
+
+ return (
+
+
+
{t("housekeeping.title")}
+
+
+
+ {/* Pending Tasks Column */}
+
+
+ {t("housekeeping.pendingTasks")} ({pendingTasks.length})
+
+ {pendingTasks.length === 0 ? (
+
+
{t("common.noResults")}
+
+ ) : (
+ pendingTasks.map((task) => (
+
+ {!task.assigned_to ? (
+
+ ) : (
+
+ )}
+ >
+ }
+ />
+ ))
+ )}
+
+
+ {/* In Progress Column */}
+
+
+ {t("housekeeping.inProgress")} ({inProgressTasks.length})
+
+ {inProgressTasks.length === 0 ? (
+
+
{t("common.noResults")}
+
+ ) : (
+ inProgressTasks.map((task) => (
+
handleComplete(task.id)}
+ >
+ {t("housekeeping.completeTask")}
+
+ }
+ />
+ ))
+ )}
+
+
+
+ {/* Staff Panel */}
+
+
+ {t("housekeeping.staffAvailability")}
+
+
+ {staff.map((s) => (
+
+
+
+
+ {s.first_name} {s.last_name}
+
+
+
0
+ ? "badge badge-warning"
+ : "badge badge-success"
+ }
+ >
+ {s.active_tasks || 0} active
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/Frontend-Hotel/src/pages/RoomService/MenuManager.jsx b/frontend/Frontend-Hotel/src/pages/RoomService/MenuManager.jsx
new file mode 100644
index 0000000..06aa925
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/pages/RoomService/MenuManager.jsx
@@ -0,0 +1,267 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ getMenu,
+ createMenuItem,
+ updateMenuItem,
+} from "../../services/roomServiceService";
+
+export default function MenuManager() {
+ const { t } = useTranslation();
+ const [menuItems, setMenuItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showModal, setShowModal] = useState(false);
+ const [form, setForm] = useState({
+ name: "",
+ name_es: "",
+ description: "",
+ price: "",
+ category: "",
+ });
+
+ const fetchMenu = async () => {
+ try {
+ const res = await getMenu();
+ setMenuItems(res.data.menu || []);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchMenu();
+ }, []);
+
+ const handleCreate = async (e) => {
+ e.preventDefault();
+ try {
+ await createMenuItem({
+ ...form,
+ price: parseFloat(form.price),
+ });
+ setShowModal(false);
+ setForm({ name: "", name_es: "", description: "", price: "", category: "" });
+ fetchMenu();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const handleToggleAvailable = async (item) => {
+ try {
+ await updateMenuItem(item.id, { available: !item.available });
+ fetchMenu();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
{t("common.loading")}
+
+ );
+ }
+
+ return (
+
+
+
{t("roomService.menuManagement")}
+
+
+
+
+
+ {menuItems.length === 0 && (
+
+
{t("common.noResults")}
+
+ )}
+
+ {/* Add Item Modal */}
+ {showModal && (
+
setShowModal(false)}>
+
e.stopPropagation()}
+ style={{ maxWidth: "500px" }}
+ >
+
{t("roomService.addItem")}
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/Frontend-Hotel/src/pages/RoomService/NewOrder.jsx b/frontend/Frontend-Hotel/src/pages/RoomService/NewOrder.jsx
new file mode 100644
index 0000000..a2e13f1
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/pages/RoomService/NewOrder.jsx
@@ -0,0 +1,384 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import { getRoomsWithStatus } from "../../services/roomService";
+import { getMenu, createRoomServiceOrder } from "../../services/roomServiceService";
+
+export default function NewOrder() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const [rooms, setRooms] = useState([]);
+ const [menuItems, setMenuItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [submitting, setSubmitting] = useState(false);
+
+ const [selectedRoom, setSelectedRoom] = useState("");
+ const [orderItems, setOrderItems] = useState([]);
+ const [notes, setNotes] = useState("");
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [roomsRes, menuRes] = await Promise.all([
+ getRoomsWithStatus(),
+ getMenu(),
+ ]);
+ const occupiedRooms = (roomsRes.data.rooms || []).filter(
+ (r) => r.status === "occupied"
+ );
+ setRooms(occupiedRooms);
+ const availableMenu = (menuRes.data.menu || []).filter(
+ (item) => item.available !== false
+ );
+ setMenuItems(availableMenu);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchData();
+ }, []);
+
+ const handleAddItem = (menuItem) => {
+ const existing = orderItems.find((oi) => oi.menu_item_id === menuItem.id);
+ if (existing) {
+ setOrderItems(
+ orderItems.map((oi) =>
+ oi.menu_item_id === menuItem.id
+ ? { ...oi, quantity: oi.quantity + 1 }
+ : oi
+ )
+ );
+ } else {
+ setOrderItems([
+ ...orderItems,
+ {
+ menu_item_id: menuItem.id,
+ name: menuItem.name,
+ price: parseFloat(menuItem.price),
+ quantity: 1,
+ },
+ ]);
+ }
+ };
+
+ const handleQuantityChange = (menuItemId, quantity) => {
+ if (quantity <= 0) {
+ setOrderItems(orderItems.filter((oi) => oi.menu_item_id !== menuItemId));
+ } else {
+ setOrderItems(
+ orderItems.map((oi) =>
+ oi.menu_item_id === menuItemId ? { ...oi, quantity } : oi
+ )
+ );
+ }
+ };
+
+ const handleRemoveItem = (menuItemId) => {
+ setOrderItems(orderItems.filter((oi) => oi.menu_item_id !== menuItemId));
+ };
+
+ const total = orderItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!selectedRoom || orderItems.length === 0) return;
+
+ setSubmitting(true);
+ try {
+ await createRoomServiceOrder({
+ room_id: selectedRoom,
+ items: orderItems.map((oi) => ({
+ menu_item_id: oi.menu_item_id,
+ quantity: oi.quantity,
+ })),
+ notes: notes || null,
+ });
+ navigate("/app/room-service");
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
{t("common.loading")}
+
+ );
+ }
+
+ // Group menu items by category
+ const categories = {};
+ menuItems.forEach((item) => {
+ const cat = item.category || "Other";
+ if (!categories[cat]) categories[cat] = [];
+ categories[cat].push(item);
+ });
+
+ return (
+
+
+
{t("roomService.newOrder")}
+
+
+
+
+
+ );
+}
diff --git a/frontend/Frontend-Hotel/src/pages/RoomService/RoomServiceOrders.jsx b/frontend/Frontend-Hotel/src/pages/RoomService/RoomServiceOrders.jsx
new file mode 100644
index 0000000..1a3e8a2
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/pages/RoomService/RoomServiceOrders.jsx
@@ -0,0 +1,226 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import {
+ getRoomServiceOrders,
+ updateRoomServiceOrderStatus,
+} from "../../services/roomServiceService";
+
+export default function RoomServiceOrders() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const fetchOrders = async () => {
+ try {
+ const res = await getRoomServiceOrders();
+ setOrders(res.data.orders || []);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchOrders();
+ }, []);
+
+ const handleStatusUpdate = async (orderId, newStatus) => {
+ try {
+ await updateRoomServiceOrderStatus(orderId, newStatus);
+ fetchOrders();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const getTimeSince = (dateStr) => {
+ if (!dateStr) return "";
+ const diff = Date.now() - new Date(dateStr).getTime();
+ const minutes = Math.floor(diff / 60000);
+ if (minutes < 60) return `${minutes}m`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h`;
+ return `${Math.floor(hours / 24)}d`;
+ };
+
+ const getStatusBadgeClass = (status) => {
+ switch (status) {
+ case "pending":
+ return "badge badge-warning";
+ case "preparing":
+ return "badge badge-info";
+ case "delivering":
+ return "badge badge-info";
+ case "delivered":
+ return "badge badge-success";
+ case "cancelled":
+ return "badge badge-error";
+ default:
+ return "badge";
+ }
+ };
+
+ const getNextAction = (status) => {
+ switch (status) {
+ case "pending":
+ return { label: t("roomService.status.preparing"), next: "preparing", className: "btn-gold" };
+ case "preparing":
+ return { label: t("roomService.status.delivering"), next: "delivering", className: "btn-gold" };
+ case "delivering":
+ return { label: t("roomService.status.delivered"), next: "delivered", className: "btn-success" };
+ default:
+ return null;
+ }
+ };
+
+ if (loading) {
+ return (
+
+
{t("common.loading")}
+
+ );
+ }
+
+ return (
+
+
+
{t("roomService.title")}
+
+
+
+
+
+
+ {orders.length === 0 ? (
+
+
{t("common.noResults")}
+
+ ) : (
+
+ {orders.map((order) => {
+ const action = getNextAction(order.status);
+ const items = Array.isArray(order.items) ? order.items.filter((i) => i.id !== null) : [];
+ return (
+
+
+
+
+ {order.name_room || `Room ${order.room_id}`}
+
+ {order.guest_name && (
+
+ {order.guest_name}
+
+ )}
+
+
+ {getTimeSince(order.created_at)}
+
+
+
+
+
+ {t(`roomService.status.${order.status}`)}
+
+
+
+ {items.length > 0 && (
+
+ {items.map((item, idx) => (
+
+
+ {item.quantity}x {item.name}
+
+ ${parseFloat(item.price || 0).toFixed(2)}
+
+ ))}
+
+ )}
+
+
+
+ {t("common.total")}: ${parseFloat(order.total || 0).toFixed(2)}
+
+ {action && (
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/frontend/Frontend-Hotel/src/services/eventService.js b/frontend/Frontend-Hotel/src/services/eventService.js
new file mode 100644
index 0000000..8e9c539
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/services/eventService.js
@@ -0,0 +1,7 @@
+import api from "./api";
+export const getVenues = () => api.get("/events/venues");
+export const createVenue = (data) => api.post("/events/venues", data);
+export const updateVenue = (id, data) => api.put(`/events/venues/${id}`, data);
+export const getEvents = (params) => api.get("/events", { params });
+export const createEvent = (data) => api.post("/events", data);
+export const updateEvent = (id, data) => api.put(`/events/${id}`, data);
diff --git a/frontend/Frontend-Hotel/src/services/housekeepingService.js b/frontend/Frontend-Hotel/src/services/housekeepingService.js
new file mode 100644
index 0000000..a443921
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/services/housekeepingService.js
@@ -0,0 +1,5 @@
+import api from "./api";
+export const getHousekeepingTasks = (params) => api.get("/housekeeping/tasks", { params });
+export const createHousekeepingTask = (data) => api.post("/housekeeping/tasks", data);
+export const updateHousekeepingTask = (id, data) => api.put(`/housekeeping/tasks/${id}`, data);
+export const getHousekeepingStaff = () => api.get("/housekeeping/staff");
diff --git a/frontend/Frontend-Hotel/src/services/roomServiceService.js b/frontend/Frontend-Hotel/src/services/roomServiceService.js
new file mode 100644
index 0000000..80dcfc3
--- /dev/null
+++ b/frontend/Frontend-Hotel/src/services/roomServiceService.js
@@ -0,0 +1,7 @@
+import api from "./api";
+export const getRoomServiceOrders = (params) => api.get("/room-service/orders", { params });
+export const createRoomServiceOrder = (data) => api.post("/room-service/orders", data);
+export const updateRoomServiceOrderStatus = (id, status) => api.put(`/room-service/orders/${id}/status`, { status });
+export const getMenu = () => api.get("/room-service/menu");
+export const createMenuItem = (data) => api.post("/room-service/menu", data);
+export const updateMenuItem = (id, data) => api.put(`/room-service/menu/${id}`, data);