From b0887286c1ed57adadef85a35bbf5b522223a06b Mon Sep 17 00:00:00 2001 From: ialcarazsalazar Date: Sun, 15 Feb 2026 01:48:23 +0000 Subject: [PATCH] feat: add housekeeping, room service, and events modules Co-Authored-By: Claude Opus 4.6 --- backend/hotel_hacienda/src/app.js | 6 + .../src/controllers/events.controller.js | 111 +++ .../controllers/housekeeping.controller.js | 109 +++ .../src/controllers/roomservice.controller.js | 130 +++ .../src/routes/events.routes.js | 12 + .../src/routes/housekeeping.routes.js | 10 + .../src/routes/roomservice.routes.js | 12 + frontend/Frontend-Hotel/src/App.jsx | 10 + .../src/constants/menuconfig.js | 10 + .../src/pages/Events/EventsVenues.jsx | 761 ++++++++++++++++++ .../src/pages/Housekeeping/Housekeeping.jsx | 370 +++++++++ .../src/pages/RoomService/MenuManager.jsx | 267 ++++++ .../src/pages/RoomService/NewOrder.jsx | 384 +++++++++ .../pages/RoomService/RoomServiceOrders.jsx | 226 ++++++ .../src/services/eventService.js | 7 + .../src/services/housekeepingService.js | 5 + .../src/services/roomServiceService.js | 7 + 17 files changed, 2437 insertions(+) create mode 100644 backend/hotel_hacienda/src/controllers/events.controller.js create mode 100644 backend/hotel_hacienda/src/controllers/housekeeping.controller.js create mode 100644 backend/hotel_hacienda/src/controllers/roomservice.controller.js create mode 100644 backend/hotel_hacienda/src/routes/events.routes.js create mode 100644 backend/hotel_hacienda/src/routes/housekeeping.routes.js create mode 100644 backend/hotel_hacienda/src/routes/roomservice.routes.js create mode 100644 frontend/Frontend-Hotel/src/pages/Events/EventsVenues.jsx create mode 100644 frontend/Frontend-Hotel/src/pages/Housekeeping/Housekeeping.jsx create mode 100644 frontend/Frontend-Hotel/src/pages/RoomService/MenuManager.jsx create mode 100644 frontend/Frontend-Hotel/src/pages/RoomService/NewOrder.jsx create mode 100644 frontend/Frontend-Hotel/src/pages/RoomService/RoomServiceOrders.jsx create mode 100644 frontend/Frontend-Hotel/src/services/eventService.js create mode 100644 frontend/Frontend-Hotel/src/services/housekeepingService.js create mode 100644 frontend/Frontend-Hotel/src/services/roomServiceService.js 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")}

+
+
+ + setVenueForm({ ...venueForm, name: e.target.value })} + /> +
+
+
+ + + setVenueForm({ ...venueForm, capacity: e.target.value }) + } + /> +
+
+ + + setVenueForm({ ...venueForm, area_sqm: e.target.value }) + } + /> +
+
+
+ + + setVenueForm({ ...venueForm, price_per_hour: e.target.value }) + } + /> +
+
+ + + setVenueForm({ ...venueForm, amenities: e.target.value }) + } + /> +
+
+ +