feat: add housekeeping, room service, and events modules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
111
backend/hotel_hacienda/src/controllers/events.controller.js
Normal file
111
backend/hotel_hacienda/src/controllers/events.controller.js
Normal file
@@ -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 };
|
||||
@@ -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 };
|
||||
130
backend/hotel_hacienda/src/controllers/roomservice.controller.js
Normal file
130
backend/hotel_hacienda/src/controllers/roomservice.controller.js
Normal file
@@ -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 };
|
||||
12
backend/hotel_hacienda/src/routes/events.routes.js
Normal file
12
backend/hotel_hacienda/src/routes/events.routes.js
Normal file
@@ -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;
|
||||
10
backend/hotel_hacienda/src/routes/housekeeping.routes.js
Normal file
10
backend/hotel_hacienda/src/routes/housekeeping.routes.js
Normal file
@@ -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;
|
||||
12
backend/hotel_hacienda/src/routes/roomservice.routes.js
Normal file
12
backend/hotel_hacienda/src/routes/roomservice.routes.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user