diff --git a/docs/plans/2026-02-15-hotel-front-office-implementation.md b/docs/plans/2026-02-15-hotel-front-office-implementation.md new file mode 100644 index 0000000..43e235c --- /dev/null +++ b/docs/plans/2026-02-15-hotel-front-office-implementation.md @@ -0,0 +1,2150 @@ +# Hotel Front-Office System Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add all front-office modules (room dashboard, reservations, guests, housekeeping, room service, events/venues, scheduling, reports) to the existing hotel management system, plus JWT auth upgrade, i18n, and dark theme. + +**Architecture:** Express 5 backend with PostgreSQL (direct SQL for new tables, consistent with existing stored-function pattern). React 19 frontend with React Router DOM 7, Tailwind 4 + new dark theme CSS tokens. JWT auth with httpOnly refresh cookies. react-i18next for bilingual ES/EN. + +**Tech Stack:** Node.js 20, Express 5, PostgreSQL 15, React 19, Vite 7, Tailwind CSS 4, jsonwebtoken, bcryptjs, react-i18next + +--- + +## Key File Paths Reference + +``` +Backend root: backend/hotel_hacienda/ +Backend src: backend/hotel_hacienda/src/ +Frontend root: frontend/Frontend-Hotel/ +Frontend src: frontend/Frontend-Hotel/src/ +``` + +**Existing patterns to follow:** +- Routes: `src/routes/.routes.js` using `express.Router()` +- Controllers: `src/controllers/.controller.js` with `pool.query()` calls +- Frontend pages: `src/pages//.jsx` +- Frontend components: `src/components//.jsx` +- Menu config: `src/constants/menuconfig.js` (label + spanish_label for each item) +- API calls: `axios` via `src/services/api.js` instance + +--- + +## Task 1: Database Schema Migration + +**Files:** +- Create: `backend/hotel_hacienda/src/db/migrations/001_front_office_tables.sql` + +**Step 1: Create migrations directory and SQL file** + +```sql +-- 001_front_office_tables.sql + +-- Add status column to existing rooms table +ALTER TABLE rooms ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'available'; +ALTER TABLE rooms ADD COLUMN IF NOT EXISTS floor INTEGER DEFAULT 1; + +-- Guests +CREATE TABLE IF NOT EXISTS guests ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + email VARCHAR(255), + phone VARCHAR(50), + id_type VARCHAR(50), + id_number VARCHAR(100), + nationality VARCHAR(100), + address TEXT, + notes TEXT, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Reservations +CREATE TABLE IF NOT EXISTS reservations ( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL, + guest_id INTEGER NOT NULL REFERENCES guests(id), + check_in DATE NOT NULL, + check_out DATE NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + channel VARCHAR(50) DEFAULT 'direct', + total_amount DECIMAL(12,2), + adults INTEGER DEFAULT 1, + children INTEGER DEFAULT 0, + notes TEXT, + created_by INTEGER, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_reservations_status ON reservations(status); +CREATE INDEX idx_reservations_dates ON reservations(check_in, check_out); +CREATE INDEX idx_reservations_room ON reservations(room_id); + +-- Guest Stay History +CREATE TABLE IF NOT EXISTS guest_stays ( + id SERIAL PRIMARY KEY, + guest_id INTEGER NOT NULL REFERENCES guests(id), + reservation_id INTEGER REFERENCES reservations(id), + room_id INTEGER NOT NULL, + check_in TIMESTAMP, + check_out TIMESTAMP, + total_charged DECIMAL(12,2), + rating INTEGER CHECK (rating >= 1 AND rating <= 5), + feedback TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Room Status Audit Log +CREATE TABLE IF NOT EXISTS room_status_log ( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL, + previous_status VARCHAR(20), + new_status VARCHAR(20), + changed_by INTEGER, + changed_at TIMESTAMP DEFAULT NOW() +); + +-- Housekeeping Tasks +CREATE TABLE IF NOT EXISTS housekeeping_tasks ( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL, + assigned_to INTEGER, + priority VARCHAR(10) DEFAULT 'normal', + type VARCHAR(20) NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + notes TEXT, + started_at TIMESTAMP, + completed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_housekeeping_status ON housekeeping_tasks(status); + +-- Menu Items (Room Service) +CREATE TABLE IF NOT EXISTS menu_items ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + name_es VARCHAR(200), + description TEXT, + description_es TEXT, + price DECIMAL(10,2) NOT NULL, + category VARCHAR(50), + available BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Room Service Orders +CREATE TABLE IF NOT EXISTS room_service_orders ( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL, + guest_id INTEGER REFERENCES guests(id), + status VARCHAR(20) DEFAULT 'pending', + total DECIMAL(10,2), + notes TEXT, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Order Items +CREATE TABLE IF NOT EXISTS order_items ( + id SERIAL PRIMARY KEY, + order_id INTEGER NOT NULL REFERENCES room_service_orders(id) ON DELETE CASCADE, + menu_item_id INTEGER NOT NULL REFERENCES menu_items(id), + quantity INTEGER NOT NULL DEFAULT 1, + price DECIMAL(10,2) NOT NULL, + notes TEXT +); + +-- Venues +CREATE TABLE IF NOT EXISTS venues ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + capacity INTEGER, + area_sqm DECIMAL(8,2), + price_per_hour DECIMAL(10,2), + amenities JSONB DEFAULT '[]', + description TEXT, + status VARCHAR(20) DEFAULT 'available', + created_at TIMESTAMP DEFAULT NOW() +); + +-- Events +CREATE TABLE IF NOT EXISTS events ( + id SERIAL PRIMARY KEY, + venue_id INTEGER NOT NULL REFERENCES venues(id), + name VARCHAR(200) NOT NULL, + organizer VARCHAR(200), + event_date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + guest_count INTEGER, + status VARCHAR(20) DEFAULT 'confirmed', + notes TEXT, + total_amount DECIMAL(12,2), + created_at TIMESTAMP DEFAULT NOW() +); + +-- Employee Schedules +CREATE TABLE IF NOT EXISTS employee_schedules ( + id SERIAL PRIMARY KEY, + employee_id INTEGER NOT NULL, + schedule_date DATE NOT NULL, + shift_type VARCHAR(20) NOT NULL, + start_time TIME, + end_time TIME, + notes TEXT, + UNIQUE(employee_id, schedule_date) +); + +-- Refresh Tokens (JWT) +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + token VARCHAR(500) NOT NULL, + expires_at TIMESTAMP NOT NULL, + revoked BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token); +CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id); +``` + +**Step 2: Run migration against database** + +Run: `docker exec -i psql -U oposgres -d haciendasanangel < backend/hotel_hacienda/src/db/migrations/001_front_office_tables.sql` + +If not using Docker yet, provide the SQL file to the user for manual execution. + +**Step 3: Commit** + +```bash +git add backend/hotel_hacienda/src/db/migrations/ +git commit -m "feat: add database migration for front-office tables" +``` + +--- + +## Task 2: JWT Authentication Upgrade + +**Files:** +- Modify: `backend/hotel_hacienda/package.json` (add jsonwebtoken, bcryptjs, cookie-parser) +- Create: `backend/hotel_hacienda/src/middlewares/authMiddleware.js` +- Modify: `backend/hotel_hacienda/src/controllers/auth.controller.js` +- Modify: `backend/hotel_hacienda/src/routes/auth.routes.js` +- Modify: `backend/hotel_hacienda/src/app.js` (add cookie-parser, apply auth middleware) +- Modify: `frontend/Frontend-Hotel/src/services/api.js` (add interceptors) +- Modify: `frontend/Frontend-Hotel/src/context/AuthContext.jsx` (token management) + +**Step 1: Install backend dependencies** + +Run: `cd backend/hotel_hacienda && npm install jsonwebtoken bcryptjs cookie-parser` + +**Step 2: Create auth middleware** + +Create `backend/hotel_hacienda/src/middlewares/authMiddleware.js`: + +```javascript +const jwt = require('jsonwebtoken'); + +const JWT_SECRET = process.env.JWT_SECRET || 'hotel-system-secret-key-change-in-production'; + +const authMiddleware = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ message: 'Token de acceso requerido' }); + } + + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; + next(); + } catch (error) { + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ message: 'Token expirado', code: 'TOKEN_EXPIRED' }); + } + return res.status(403).json({ message: 'Token invalido' }); + } +}; + +module.exports = { authMiddleware, JWT_SECRET }; +``` + +**Step 3: Update auth controller to issue JWT tokens** + +Replace `backend/hotel_hacienda/src/controllers/auth.controller.js` login function to: +- On successful login, generate access token (15min) and refresh token (7 days) +- Store refresh token in DB and set as httpOnly cookie +- Add `refreshToken` function +- Add `logoutUser` function + +```javascript +const pool = require('../db/connection'); +const transporter = require('../services/mailService'); +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); +const { JWT_SECRET } = require('../middlewares/authMiddleware'); + +const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'hotel-refresh-secret-change-in-production'; + +function generateAccessToken(payload) { + return jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' }); +} + +function generateRefreshToken(payload) { + return jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' }); +} + +const login = async (req, res) => { + const { name_mail_user, user_pass } = req.body; + try { + const result = await pool.query('SELECT * from validarusuario($1, $2)', [name_mail_user, user_pass]); + const { status, rol, user_id, user_name } = result.rows[0]; + + if (status !== 1) { + return res.json({ message: 'Usuario o contrasena incorrectos', status }); + } + + const tokenPayload = { user_id, user_name, rol }; + const accessToken = generateAccessToken(tokenPayload); + const refreshToken = generateRefreshToken(tokenPayload); + + // Store refresh token in DB + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + await pool.query( + 'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES ($1, $2, $3)', + [user_id, refreshToken, expiresAt] + ); + + // Set refresh token as httpOnly cookie + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 + }); + + res.json({ + rol, + user_id, + user_name, + accessToken, + message: 'Usuario autenticado correctamente' + }); + } catch (error) { + console.error(error); + res.status(500).json({ status: 500, message: 'Error interno del servidor' }); + } +}; + +const refreshTokenHandler = async (req, res) => { + const token = req.cookies?.refreshToken; + if (!token) { + return res.status(401).json({ message: 'Refresh token requerido' }); + } + + try { + const decoded = jwt.verify(token, REFRESH_SECRET); + + // Check if token exists in DB and is not revoked + const result = await pool.query( + 'SELECT * FROM refresh_tokens WHERE token = $1 AND revoked = false AND expires_at > NOW()', + [token] + ); + + if (result.rows.length === 0) { + return res.status(403).json({ message: 'Refresh token invalido o revocado' }); + } + + const tokenPayload = { user_id: decoded.user_id, user_name: decoded.user_name, rol: decoded.rol }; + const newAccessToken = generateAccessToken(tokenPayload); + + res.json({ accessToken: newAccessToken }); + } catch (error) { + return res.status(403).json({ message: 'Refresh token invalido' }); + } +}; + +const logoutUser = async (req, res) => { + const token = req.cookies?.refreshToken; + if (token) { + await pool.query('UPDATE refresh_tokens SET revoked = true WHERE token = $1', [token]); + res.clearCookie('refreshToken'); + } + res.json({ message: 'Sesion cerrada correctamente' }); +}; + +// Keep existing createuser and passRecover unchanged... +``` + +**Step 4: Update auth routes** + +Add to `backend/hotel_hacienda/src/routes/auth.routes.js`: +```javascript +router.post('/refresh', authController.refreshTokenHandler); +router.post('/logout', authController.logoutUser); +``` + +**Step 5: Update app.js to use cookie-parser and auth middleware on protected routes** + +In `backend/hotel_hacienda/src/app.js`, add: +```javascript +const cookieParser = require('cookie-parser'); +const { authMiddleware } = require('./middlewares/authMiddleware'); + +app.use(cookieParser()); + +// Apply authMiddleware to all routes EXCEPT auth +// Add before route registration: +app.use('/api/employees', authMiddleware, employeeRoutes); +app.use('/api/contracts', authMiddleware, contractsRoutes); +// ... apply to all routes except /api/auth +``` + +Update `corsOptions` to include `credentials: true`. + +**Step 6: Update frontend API service with token interceptors** + +Update `frontend/Frontend-Hotel/src/services/api.js`: + +```javascript +import axios from "axios"; + +const api = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 15000, + withCredentials: true, +}); + +// Request interceptor: attach access token +api.interceptors.request.use((config) => { + const token = sessionStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Response interceptor: handle 401 and refresh +api.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + if (error.response?.status === 401 && error.response?.data?.code === 'TOKEN_EXPIRED' && !originalRequest._retry) { + originalRequest._retry = true; + try { + const { data } = await axios.post( + `${import.meta.env.VITE_API_BASE_URL}/auth/refresh`, + {}, + { withCredentials: true } + ); + sessionStorage.setItem('accessToken', data.accessToken); + originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; + return api(originalRequest); + } catch (refreshError) { + sessionStorage.removeItem('accessToken'); + window.location.href = '/'; + return Promise.reject(refreshError); + } + } + return Promise.reject(error); + } +); + +export default api; +``` + +**Step 7: Update AuthContext for JWT** + +Update `frontend/Frontend-Hotel/src/context/AuthContext.jsx` to: +- Store access token in sessionStorage (not localStorage) +- Store user data (role, name) in state +- Call `/api/auth/logout` on logout +- Remove old localStorage "rol" approach + +```javascript +import { createContext, useState, useEffect } from "react"; +import api from "../services/api"; + +export const AuthContext = createContext(); + +function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = sessionStorage.getItem("accessToken"); + const savedUser = sessionStorage.getItem("userRole"); + const savedUserData = sessionStorage.getItem("userData"); + if (token && savedUser) { + setUser(JSON.parse(savedUser)); + if (savedUserData) setUserData(JSON.parse(savedUserData)); + } + setLoading(false); + }, []); + + const login = (role, data, accessToken) => { + setUser(role); + setUserData(data); + sessionStorage.setItem("accessToken", accessToken); + sessionStorage.setItem("userRole", JSON.stringify(role)); + sessionStorage.setItem("userData", JSON.stringify(data)); + }; + + const logout = async () => { + try { + await api.post("/auth/logout"); + } catch (e) { + // ignore + } + setUser(null); + setUserData(null); + sessionStorage.removeItem("accessToken"); + sessionStorage.removeItem("userRole"); + sessionStorage.removeItem("userData"); + }; + + return ( + + {children} + + ); +} + +export { AuthProvider }; +``` + +**Step 8: Update Login page to use new JWT response** + +In the Login page, update the login handler to call `login(rol, { user_id, user_name }, accessToken)` using the new response shape. + +**Step 9: Add JWT_SECRET and JWT_REFRESH_SECRET to .env.example** + +``` +JWT_SECRET=your-jwt-secret-here +JWT_REFRESH_SECRET=your-jwt-refresh-secret-here +``` + +**Step 10: Commit** + +```bash +git add -A +git commit -m "feat: upgrade authentication to JWT with refresh tokens" +``` + +--- + +## Task 3: i18n Setup with react-i18next + +**Files:** +- Modify: `frontend/Frontend-Hotel/package.json` (add i18next packages) +- Create: `frontend/Frontend-Hotel/src/i18n/index.js` +- Create: `frontend/Frontend-Hotel/src/i18n/locales/en.json` +- Create: `frontend/Frontend-Hotel/src/i18n/locales/es.json` +- Modify: `frontend/Frontend-Hotel/src/main.jsx` (add I18nextProvider) +- Modify: `frontend/Frontend-Hotel/src/context/LenguageContext.jsx` (integrate with i18next) + +**Step 1: Install i18n packages** + +Run: `cd frontend/Frontend-Hotel && npm install react-i18next i18next i18next-browser-languagedetector` + +**Step 2: Create i18n configuration** + +Create `frontend/Frontend-Hotel/src/i18n/index.js`: + +```javascript +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import en from './locales/en.json'; +import es from './locales/es.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + es: { translation: es }, + }, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; +``` + +**Step 3: Create English locale file** + +Create `frontend/Frontend-Hotel/src/i18n/locales/en.json`: + +```json +{ + "nav": { + "dashboards": "Dashboards", + "roomDashboard": "Room Dashboard", + "reservations": "Reservations", + "guests": "Guests", + "housekeeping": "Housekeeping", + "roomService": "Room Service", + "eventsVenues": "Events & Venues", + "schedules": "Schedules", + "operationalReports": "Operational Reports", + "income": "Income", + "expenses": "Expenses", + "expensesApproval": "Expenses to be approved", + "inventory": "Inventory", + "payroll": "Payroll", + "hotel": "Hotel", + "housekeeper": "Housekeeper", + "services": "Services", + "operations": "Operations", + "staff": "Staff" + }, + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "create": "Create", + "search": "Search", + "filter": "Filter", + "loading": "Loading...", + "noResults": "No results found", + "confirm": "Confirm", + "back": "Back", + "actions": "Actions", + "status": "Status", + "date": "Date", + "total": "Total", + "notes": "Notes" + }, + "rooms": { + "title": "Room Dashboard", + "available": "Available", + "occupied": "Occupied", + "cleaning": "Cleaning", + "maintenance": "Maintenance", + "occupancyRate": "Occupancy Rate", + "availableRooms": "Available Rooms", + "todayCheckIns": "Today's Check-ins", + "todayCheckOuts": "Today's Check-outs", + "dailyRevenue": "Daily Revenue", + "floor": "Floor", + "roomNumber": "Room Number", + "roomType": "Room Type", + "pricePerNight": "Price per Night", + "amenities": "Amenities", + "guestInfo": "Guest Information" + }, + "reservations": { + "title": "Reservations", + "newReservation": "New Reservation", + "checkIn": "Check-in", + "checkOut": "Check-out", + "guestName": "Guest Name", + "roomType": "Room Type", + "channel": "Channel", + "duration": "Duration", + "nights": "nights", + "status": { + "pending": "Pending", + "confirmed": "Confirmed", + "checkedIn": "Checked In", + "checkedOut": "Checked Out", + "cancelled": "Cancelled" + }, + "channels": { + "direct": "Direct", + "booking": "Booking.com", + "expedia": "Expedia", + "airbnb": "Airbnb", + "other": "Other" + } + }, + "guests": { + "title": "Guests", + "newGuest": "New Guest", + "firstName": "First Name", + "lastName": "Last Name", + "email": "Email", + "phone": "Phone", + "idType": "ID Type", + "idNumber": "ID Number", + "nationality": "Nationality", + "address": "Address", + "stayHistory": "Stay History", + "currentRoom": "Current Room" + }, + "housekeeping": { + "title": "Housekeeping", + "pendingTasks": "Pending Tasks", + "inProgress": "In Progress", + "completedTasks": "Completed", + "assignTo": "Assign to", + "priority": { + "high": "High", + "normal": "Normal", + "low": "Low" + }, + "type": { + "checkout": "Checkout", + "maintenance": "Maintenance", + "deepClean": "Deep Clean", + "turndown": "Turndown" + }, + "startTask": "Start", + "completeTask": "Complete", + "staffAvailability": "Staff Availability" + }, + "roomService": { + "title": "Room Service", + "activeOrders": "Active Orders", + "orderHistory": "Order History", + "newOrder": "New Order", + "menuManagement": "Menu Management", + "preparing": "Preparing", + "delivering": "Delivering", + "delivered": "Delivered", + "menuItem": "Menu Item", + "price": "Price", + "quantity": "Quantity", + "category": "Category" + }, + "events": { + "title": "Events & Venues", + "venues": "Venues", + "upcomingEvents": "Upcoming Events", + "newEvent": "New Event", + "venueName": "Venue Name", + "capacity": "Capacity", + "area": "Area", + "pricePerHour": "Price per Hour", + "eventName": "Event Name", + "organizer": "Organizer", + "guestCount": "Guest Count", + "eventDate": "Date", + "startTime": "Start Time", + "endTime": "End Time" + }, + "schedules": { + "title": "Schedules", + "shifts": { + "morning": "Morning", + "afternoon": "Afternoon", + "night": "Night", + "off": "Off" + }, + "department": "Department", + "employee": "Employee", + "week": "Week" + }, + "reports": { + "title": "Operational Reports", + "occupancyRate": "Occupancy Rate", + "revenue": "Revenue", + "guestSatisfaction": "Guest Satisfaction", + "bookingSources": "Booking Sources", + "period": { + "week": "Week", + "month": "Month", + "quarter": "Quarter", + "year": "Year" + }, + "vsTarget": "vs Target", + "trend": "Trend" + }, + "auth": { + "login": "Login", + "logout": "Logout", + "email": "Email", + "password": "Password", + "forgotPassword": "Forgot Password?" + } +} +``` + +**Step 4: Create Spanish locale file** + +Create `frontend/Frontend-Hotel/src/i18n/locales/es.json`: + +```json +{ + "nav": { + "dashboards": "Tableros", + "roomDashboard": "Panel de Habitaciones", + "reservations": "Reservaciones", + "guests": "Huespedes", + "housekeeping": "Limpieza", + "roomService": "Servicio a Habitacion", + "eventsVenues": "Eventos y Salones", + "schedules": "Horarios", + "operationalReports": "Reportes Operativos", + "income": "Ingresos", + "expenses": "Gastos", + "expensesApproval": "Gastos por aprobar", + "inventory": "Inventario", + "payroll": "Nomina", + "hotel": "Hotel", + "housekeeper": "Cuidador de Habitaciones", + "services": "Servicios", + "operations": "Operaciones", + "staff": "Personal" + }, + "common": { + "save": "Guardar", + "cancel": "Cancelar", + "delete": "Eliminar", + "edit": "Editar", + "create": "Crear", + "search": "Buscar", + "filter": "Filtrar", + "loading": "Cargando...", + "noResults": "Sin resultados", + "confirm": "Confirmar", + "back": "Regresar", + "actions": "Acciones", + "status": "Estado", + "date": "Fecha", + "total": "Total", + "notes": "Notas" + }, + "rooms": { + "title": "Panel de Habitaciones", + "available": "Disponible", + "occupied": "Ocupada", + "cleaning": "Limpieza", + "maintenance": "Mantenimiento", + "occupancyRate": "Tasa de Ocupacion", + "availableRooms": "Habitaciones Disponibles", + "todayCheckIns": "Check-ins de Hoy", + "todayCheckOuts": "Check-outs de Hoy", + "dailyRevenue": "Ingreso Diario", + "floor": "Piso", + "roomNumber": "Numero de Habitacion", + "roomType": "Tipo de Habitacion", + "pricePerNight": "Precio por Noche", + "amenities": "Amenidades", + "guestInfo": "Informacion del Huesped" + }, + "reservations": { + "title": "Reservaciones", + "newReservation": "Nueva Reservacion", + "checkIn": "Entrada", + "checkOut": "Salida", + "guestName": "Nombre del Huesped", + "roomType": "Tipo de Habitacion", + "channel": "Canal", + "duration": "Duracion", + "nights": "noches", + "status": { + "pending": "Pendiente", + "confirmed": "Confirmada", + "checkedIn": "Registrado", + "checkedOut": "Check-out", + "cancelled": "Cancelada" + }, + "channels": { + "direct": "Directo", + "booking": "Booking.com", + "expedia": "Expedia", + "airbnb": "Airbnb", + "other": "Otro" + } + }, + "guests": { + "title": "Huespedes", + "newGuest": "Nuevo Huesped", + "firstName": "Nombre", + "lastName": "Apellido", + "email": "Correo", + "phone": "Telefono", + "idType": "Tipo de ID", + "idNumber": "Numero de ID", + "nationality": "Nacionalidad", + "address": "Direccion", + "stayHistory": "Historial de Estadias", + "currentRoom": "Habitacion Actual" + }, + "housekeeping": { + "title": "Limpieza", + "pendingTasks": "Tareas Pendientes", + "inProgress": "En Progreso", + "completedTasks": "Completadas", + "assignTo": "Asignar a", + "priority": { + "high": "Alta", + "normal": "Normal", + "low": "Baja" + }, + "type": { + "checkout": "Check-out", + "maintenance": "Mantenimiento", + "deepClean": "Limpieza Profunda", + "turndown": "Preparacion Nocturna" + }, + "startTask": "Iniciar", + "completeTask": "Completar", + "staffAvailability": "Disponibilidad del Personal" + }, + "roomService": { + "title": "Servicio a Habitacion", + "activeOrders": "Ordenes Activas", + "orderHistory": "Historial de Ordenes", + "newOrder": "Nueva Orden", + "menuManagement": "Gestion del Menu", + "preparing": "Preparando", + "delivering": "Entregando", + "delivered": "Entregado", + "menuItem": "Platillo", + "price": "Precio", + "quantity": "Cantidad", + "category": "Categoria" + }, + "events": { + "title": "Eventos y Salones", + "venues": "Salones", + "upcomingEvents": "Proximos Eventos", + "newEvent": "Nuevo Evento", + "venueName": "Nombre del Salon", + "capacity": "Capacidad", + "area": "Area", + "pricePerHour": "Precio por Hora", + "eventName": "Nombre del Evento", + "organizer": "Organizador", + "guestCount": "Numero de Invitados", + "eventDate": "Fecha", + "startTime": "Hora de Inicio", + "endTime": "Hora de Fin" + }, + "schedules": { + "title": "Horarios", + "shifts": { + "morning": "Matutino", + "afternoon": "Vespertino", + "night": "Nocturno", + "off": "Descanso" + }, + "department": "Departamento", + "employee": "Empleado", + "week": "Semana" + }, + "reports": { + "title": "Reportes Operativos", + "occupancyRate": "Tasa de Ocupacion", + "revenue": "Ingresos", + "guestSatisfaction": "Satisfaccion del Huesped", + "bookingSources": "Fuentes de Reservacion", + "period": { + "week": "Semana", + "month": "Mes", + "quarter": "Trimestre", + "year": "Ano" + }, + "vsTarget": "vs Objetivo", + "trend": "Tendencia" + }, + "auth": { + "login": "Iniciar Sesion", + "logout": "Cerrar Sesion", + "email": "Correo", + "password": "Contrasena", + "forgotPassword": "Olvido su contrasena?" + } +} +``` + +**Step 5: Initialize i18n in main.jsx** + +Add `import './i18n';` at the top of `frontend/Frontend-Hotel/src/main.jsx` (before App import). + +**Step 6: Update LenguageContext to use i18next** + +Update `frontend/Frontend-Hotel/src/context/LenguageContext.jsx`: + +```javascript +import { createContext, useContext } from "react"; +import { useTranslation } from "react-i18next"; + +export const langContext = createContext(); + +export const LangProvider = ({ children }) => { + const { i18n } = useTranslation(); + const lang = i18n.language?.startsWith('es') ? 'es' : 'en'; + + const toggleLang = (event) => { + const newLang = event.target.value; + i18n.changeLanguage(newLang); + }; + + return ( + + {children} + + ); +}; + +export const useLang = () => useContext(langContext); +``` + +**Step 7: Commit** + +```bash +git add -A +git commit -m "feat: add react-i18next bilingual support (ES/EN)" +``` + +--- + +## Task 4: Dark Theme Design System + +**Files:** +- Create: `frontend/Frontend-Hotel/src/styles/theme.css` +- Modify: `frontend/Frontend-Hotel/src/styles/Dashboard.css` (update sidebar, topbar, content) +- Modify: `frontend/Frontend-Hotel/src/styles/global.css` (update base styles) + +**Step 1: Create theme.css with CSS custom properties** + +Create `frontend/Frontend-Hotel/src/styles/theme.css`: + +```css +:root { + /* Background layers */ + --bg-primary: #0a0a0a; + --bg-secondary: #1a1a1a; + --bg-elevated: #2a2a2a; + --bg-surface: #1e1e1e; + + /* Accent */ + --accent-gold: #d4a574; + --accent-gold-hover: #e0b88a; + --accent-gold-muted: rgba(212, 165, 116, 0.15); + + /* Text */ + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --text-muted: #666666; + + /* Status colors */ + --status-available: #22c55e; + --status-occupied: #3b82f6; + --status-cleaning: #eab308; + --status-maintenance: #ef4444; + + /* Semantic colors */ + --success: #22c55e; + --warning: #eab308; + --error: #ef4444; + --info: #3b82f6; + + /* Borders */ + --border-color: #333333; + --border-subtle: #2a2a2a; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5); + + /* Border radius */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + + /* Typography */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; +} + +/* Base resets for dark theme */ +body { + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: var(--font-sans); + margin: 0; +} + +/* Utility classes for new components */ +.card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--space-lg); +} + +.card-elevated { + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--space-lg); +} + +.badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.badge-available { background: rgba(34, 197, 94, 0.15); color: var(--status-available); } +.badge-occupied { background: rgba(59, 130, 246, 0.15); color: var(--status-occupied); } +.badge-cleaning { background: rgba(234, 179, 8, 0.15); color: var(--status-cleaning); } +.badge-maintenance { background: rgba(239, 68, 68, 0.15); color: var(--status-maintenance); } +.badge-pending { background: rgba(234, 179, 8, 0.15); color: var(--status-cleaning); } +.badge-confirmed { background: rgba(59, 130, 246, 0.15); color: var(--status-occupied); } +.badge-success { background: rgba(34, 197, 94, 0.15); color: var(--success); } +.badge-error { background: rgba(239, 68, 68, 0.15); color: var(--error); } + +.kpi-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--space-lg); + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.kpi-card .kpi-value { + font-size: 2rem; + font-weight: 700; + color: var(--text-primary); +} + +.kpi-card .kpi-label { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.kpi-card .kpi-trend { + font-size: 0.75rem; + font-weight: 600; +} + +.kpi-trend.positive { color: var(--success); } +.kpi-trend.negative { color: var(--error); } + +/* Button styles */ +.btn-gold { + background: var(--accent-gold); + color: #000; + border: none; + padding: 10px 20px; + border-radius: var(--radius-md); + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.btn-gold:hover { + background: var(--accent-gold-hover); +} + +.btn-outline { + background: transparent; + color: var(--text-primary); + border: 1px solid var(--border-color); + padding: 10px 20px; + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.2s; +} + +.btn-outline:hover { + border-color: var(--accent-gold); + color: var(--accent-gold); +} + +/* Form inputs dark theme */ +.input-dark { + background: var(--bg-elevated); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 10px 14px; + border-radius: var(--radius-md); + font-size: 0.875rem; + width: 100%; + outline: none; + transition: border-color 0.2s; +} + +.input-dark:focus { + border-color: var(--accent-gold); +} + +.input-dark::placeholder { + color: var(--text-muted); +} + +/* Table dark theme */ +.table-dark { + width: 100%; + border-collapse: collapse; +} + +.table-dark th { + text-align: left; + padding: 12px 16px; + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border-color); +} + +.table-dark td { + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); + color: var(--text-primary); + font-size: 0.875rem; +} + +.table-dark tr:hover { + background: var(--bg-elevated); +} + +/* Grid layout helpers */ +.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-lg); } +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-lg); } +.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-lg); } +.grid-5 { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--space-lg); } + +@media (max-width: 1024px) { + .grid-4, .grid-5 { grid-template-columns: repeat(2, 1fr); } + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 640px) { + .grid-2, .grid-3, .grid-4, .grid-5 { grid-template-columns: 1fr; } +} +``` + +**Step 2: Update Dashboard.css for dark theme** + +Update sidebar gradient, topbar, content background to use CSS variables. Change: +- Sidebar: `background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary))` +- Topbar: `background-color: var(--bg-secondary)` with gold accent border-bottom +- Content area: `background-color: var(--bg-primary)` +- Submenu links: gold accent styling + +**Step 3: Import theme.css in global.css** + +Add `@import './theme.css';` at the top of `frontend/Frontend-Hotel/src/styles/global.css`. + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add dark theme design system with gold accents" +``` + +--- + +## Task 5: Room Dashboard & Status Management (Backend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/rooms.controller.js` +- Create: `backend/hotel_hacienda/src/routes/rooms.routes.js` +- Create: `backend/hotel_hacienda/src/controllers/dashboard.controller.js` +- Create: `backend/hotel_hacienda/src/routes/dashboard.routes.js` +- Modify: `backend/hotel_hacienda/src/app.js` (register new routes) + +**Step 1: Create rooms controller** + +Create `backend/hotel_hacienda/src/controllers/rooms.controller.js`: + +```javascript +const pool = require('../db/connection'); + +const getRoomsWithStatus = async (req, res) => { + try { + const result = await pool.query(` + SELECT r.*, + res.id as reservation_id, + g.first_name || ' ' || g.last_name as guest_name, + g.phone as guest_phone, + res.check_in, + res.check_out + FROM rooms r + LEFT JOIN reservations res ON res.room_id = r.id_room AND res.status = 'checked_in' + LEFT JOIN guests g ON g.id = res.guest_id + ORDER BY r.floor, r.name_room + `); + // Note: adjust column names based on actual rooms table schema + res.json({ rooms: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener estado de habitaciones' }); + } +}; + +const updateRoomStatus = async (req, res) => { + try { + const { id } = req.params; + const { status } = req.body; + const userId = req.user?.user_id; + + const validStatuses = ['available', 'occupied', 'cleaning', 'maintenance']; + if (!validStatuses.includes(status)) { + return res.status(400).json({ message: 'Estado invalido' }); + } + + // Get current status for audit log + const current = await pool.query('SELECT status FROM rooms WHERE id_room = $1', [id]); + if (current.rows.length === 0) { + return res.status(404).json({ message: 'Habitacion no encontrada' }); + } + + const previousStatus = current.rows[0].status; + + // Update room status + await pool.query('UPDATE rooms SET status = $1 WHERE id_room = $2', [status, id]); + + // Log status change + await pool.query( + 'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)', + [id, previousStatus, status, userId] + ); + + // If set to "cleaning", auto-create housekeeping task + if (status === 'cleaning') { + await pool.query( + `INSERT INTO housekeeping_tasks (room_id, priority, type, status) VALUES ($1, 'high', 'checkout', 'pending')`, + [id] + ); + } + + res.json({ message: 'Estado actualizado correctamente' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al actualizar estado' }); + } +}; + +module.exports = { getRoomsWithStatus, updateRoomStatus }; +``` + +**Step 2: Create rooms routes** + +Create `backend/hotel_hacienda/src/routes/rooms.routes.js`: + +```javascript +const express = require('express'); +const router = express.Router(); +const roomsController = require('../controllers/rooms.controller'); + +router.get('/status', roomsController.getRoomsWithStatus); +router.put('/:id/status', roomsController.updateRoomStatus); + +module.exports = router; +``` + +**Step 3: Create dashboard controller** + +Create `backend/hotel_hacienda/src/controllers/dashboard.controller.js`: + +```javascript +const pool = require('../db/connection'); + +const getKPIs = async (req, res) => { + try { + const today = new Date().toISOString().split('T')[0]; + + const [totalRooms, availableRooms, checkIns, checkOuts] = await Promise.all([ + pool.query('SELECT COUNT(*) as count FROM rooms'), + pool.query("SELECT COUNT(*) as count FROM rooms WHERE status = 'available'"), + pool.query("SELECT COUNT(*) as count FROM reservations WHERE check_in = $1 AND status IN ('confirmed', 'checked_in')", [today]), + pool.query("SELECT COUNT(*) as count FROM reservations WHERE check_out = $1 AND status = 'checked_in'", [today]), + ]); + + const total = parseInt(totalRooms.rows[0].count); + const available = parseInt(availableRooms.rows[0].count); + const occupancy = total > 0 ? Math.round(((total - available) / total) * 100) : 0; + + res.json({ + occupancy, + availableRooms: available, + totalRooms: total, + todayCheckIns: parseInt(checkIns.rows[0].count), + todayCheckOuts: parseInt(checkOuts.rows[0].count), + }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener KPIs' }); + } +}; + +const getWeeklyRevenue = async (req, res) => { + try { + const result = await pool.query(` + SELECT + DATE(check_out) as day, + COALESCE(SUM(total_amount), 0) as revenue + FROM reservations + WHERE check_out >= CURRENT_DATE - INTERVAL '7 days' + AND status = 'checked_out' + GROUP BY DATE(check_out) + ORDER BY day + `); + res.json({ weeklyRevenue: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener ingresos semanales' }); + } +}; + +const getTodayArrivals = async (req, res) => { + try { + const today = new Date().toISOString().split('T')[0]; + const result = await pool.query(` + SELECT r.id, r.check_in, r.check_out, r.room_id, r.status, + g.first_name, g.last_name, g.phone + FROM reservations r + JOIN guests g ON g.id = r.guest_id + WHERE (r.check_in = $1 OR r.check_out = $1) + AND r.status IN ('confirmed', 'checked_in') + ORDER BY r.check_in + `, [today]); + res.json({ arrivals: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener llegadas' }); + } +}; + +module.exports = { getKPIs, getWeeklyRevenue, getTodayArrivals }; +``` + +**Step 4: Create dashboard routes** + +Create `backend/hotel_hacienda/src/routes/dashboard.routes.js`: + +```javascript +const express = require('express'); +const router = express.Router(); +const dashboardController = require('../controllers/dashboard.controller'); + +router.get('/kpis', dashboardController.getKPIs); +router.get('/weekly-revenue', dashboardController.getWeeklyRevenue); +router.get('/today-arrivals', dashboardController.getTodayArrivals); + +module.exports = router; +``` + +**Step 5: Register routes in app.js** + +Add to `backend/hotel_hacienda/src/app.js`: + +```javascript +const roomsRoutes = require('./routes/rooms.routes'); +const dashboardRoutes = require('./routes/dashboard.routes'); + +app.use('/api/rooms', authMiddleware, roomsRoutes); +app.use('/api/dashboard', authMiddleware, dashboardRoutes); +``` + +**Step 6: Commit** + +```bash +git add -A +git commit -m "feat: add room status and dashboard KPI API endpoints" +``` + +--- + +## Task 6: Room Dashboard (Frontend) + +**Files:** +- Create: `frontend/Frontend-Hotel/src/pages/RoomDashboard/RoomDashboard.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/RoomDashboard/components/KPIBar.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomGrid.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomDetailModal.jsx` +- Create: `frontend/Frontend-Hotel/src/services/roomService.js` +- Modify: `frontend/Frontend-Hotel/src/App.jsx` (add route) +- Modify: `frontend/Frontend-Hotel/src/constants/menuconfig.js` (add menu entry) + +**Step 1: Create room service** + +Create `frontend/Frontend-Hotel/src/services/roomService.js`: + +```javascript +import api from "./api"; + +export const getRoomsWithStatus = () => api.get("/rooms/status"); +export const updateRoomStatus = (id, status) => api.put(`/rooms/${id}/status`, { status }); +export const getDashboardKPIs = () => api.get("/dashboard/kpis"); +export const getWeeklyRevenue = () => api.get("/dashboard/weekly-revenue"); +export const getTodayArrivals = () => api.get("/dashboard/today-arrivals"); +``` + +**Step 2: Create KPIBar component** + +Create `frontend/Frontend-Hotel/src/pages/RoomDashboard/components/KPIBar.jsx`: + +A horizontal bar with 4-5 KPI cards showing occupancy %, available rooms, today's check-ins, today's check-outs. Use `kpi-card` class from theme.css. Use `useTranslation()` for labels. + +**Step 3: Create RoomGrid component** + +Create `frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomGrid.jsx`: + +Grid of room cards grouped by floor. Each card shows room number, status color indicator, guest name if occupied, price. Color coding: +- Available: `var(--status-available)` border-left +- Occupied: `var(--status-occupied)` border-left +- Cleaning: `var(--status-cleaning)` border-left +- Maintenance: `var(--status-maintenance)` border-left + +Click opens RoomDetailModal. + +**Step 4: Create RoomDetailModal component** + +Create `frontend/Frontend-Hotel/src/pages/RoomDashboard/components/RoomDetailModal.jsx`: + +Modal showing full room details: room number, type, guest info, amenities, current reservation dates, price per night. Include status change buttons. + +**Step 5: Create RoomDashboard page** + +Create `frontend/Frontend-Hotel/src/pages/RoomDashboard/RoomDashboard.jsx`: + +```javascript +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import KPIBar from "./components/KPIBar"; +import RoomGrid from "./components/RoomGrid"; +import RoomDetailModal from "./components/RoomDetailModal"; +import { getRoomsWithStatus, getDashboardKPIs, updateRoomStatus } from "../../services/roomService"; +import "../../styles/theme.css"; + +export default function RoomDashboard() { + const { t } = useTranslation(); + const [rooms, setRooms] = useState([]); + const [kpis, setKpis] = useState({}); + const [selectedRoom, setSelectedRoom] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchData = async () => { + try { + const [roomsRes, kpisRes] = await Promise.all([ + getRoomsWithStatus(), + getDashboardKPIs(), + ]); + setRooms(roomsRes.data.rooms); + setKpis(kpisRes.data); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchData(); }, []); + + const handleStatusChange = async (roomId, newStatus) => { + await updateRoomStatus(roomId, newStatus); + fetchData(); + setSelectedRoom(null); + }; + + if (loading) return

{t('common.loading')}

; + + return ( +
+

+ {t('rooms.title')} +

+ + + {selectedRoom && ( + setSelectedRoom(null)} + onStatusChange={handleStatusChange} + /> + )} +
+ ); +} +``` + +**Step 6: Add route in App.jsx** + +Add import and route: +```javascript +import RoomDashboard from "./pages/RoomDashboard/RoomDashboard.jsx"; +// Inside : +} /> +``` + +**Step 7: Add to menu config** + +Add to `frontend/Frontend-Hotel/src/constants/menuconfig.js`: + +```javascript +operations: { + label: "Operations", + spanish_label: "Operaciones", + basePath: "/app/room-dashboard", + submenu: [ + { label: "Room Dashboard", spanish_label: "Panel de Habitaciones", route: "/app/room-dashboard" }, + { label: "Reservations", spanish_label: "Reservaciones", route: "/app/reservations" }, + { label: "Guests", spanish_label: "Huespedes", route: "/app/guests" }, + ], +}, +``` + +**Step 8: Commit** + +```bash +git add -A +git commit -m "feat: add room dashboard with KPI bar and floor grid" +``` + +--- + +## Task 7: Reservations Module (Backend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/reservations.controller.js` +- Create: `backend/hotel_hacienda/src/routes/reservations.routes.js` +- Modify: `backend/hotel_hacienda/src/app.js` + +**Step 1: Create reservations controller** + +Create `backend/hotel_hacienda/src/controllers/reservations.controller.js` with: + +- `getReservations(req, res)` - GET with query params: status, from_date, to_date, search (guest name). Uses JOIN on guests table. +- `createReservation(req, res)` - POST. Validates room availability (no overlapping checked_in/confirmed reservations for same room and date range). Creates guest if guest_id not provided (inline guest creation). Returns new reservation. +- `updateReservation(req, res)` - PUT /:id. Updates editable fields. +- `updateReservationStatus(req, res)` - PUT /:id/status. State machine validation: + - pending -> confirmed, cancelled + - confirmed -> checked_in, cancelled + - checked_in -> checked_out + - On checked_in: set room status to 'occupied', create guest_stay record + - On checked_out: set room status to 'cleaning', create housekeeping task, update guest_stay check_out +- `checkAvailability(req, res)` - GET /availability?room_id=&check_in=&check_out=. Returns boolean. + +**Step 2: Create reservations routes** + +```javascript +const express = require('express'); +const router = express.Router(); +const ctrl = require('../controllers/reservations.controller'); + +router.get('/', ctrl.getReservations); +router.post('/', ctrl.createReservation); +router.put('/:id', ctrl.updateReservation); +router.put('/:id/status', ctrl.updateReservationStatus); +router.get('/availability', ctrl.checkAvailability); + +module.exports = router; +``` + +**Step 3: Register in app.js** + +```javascript +const reservationsRoutes = require('./routes/reservations.routes'); +app.use('/api/reservations', authMiddleware, reservationsRoutes); +``` + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add reservations CRUD with status state machine" +``` + +--- + +## Task 8: Reservations Module (Frontend) + +**Files:** +- Create: `frontend/Frontend-Hotel/src/pages/Reservations/Reservations.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Reservations/NewReservation.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Reservations/components/ReservationCard.jsx` +- Create: `frontend/Frontend-Hotel/src/services/reservationService.js` +- Modify: `frontend/Frontend-Hotel/src/App.jsx` + +**Step 1: Create reservation service** + +```javascript +import api from "./api"; + +export const getReservations = (params) => api.get("/reservations", { params }); +export const createReservation = (data) => api.post("/reservations", data); +export const updateReservation = (id, data) => api.put(`/reservations/${id}`, data); +export const updateReservationStatus = (id, status) => api.put(`/reservations/${id}/status`, { status }); +export const checkAvailability = (params) => api.get("/reservations/availability", { params }); +``` + +**Step 2: Create ReservationCard component** + +Card showing: guest name, room, dates (check-in/out), duration in nights, total amount, status badge, channel badge, phone. Status-contextual action buttons (Confirm, Check-in, Check-out, Cancel). + +**Step 3: Create Reservations list page** + +Filterable card grid with status tabs (All, Pending, Confirmed, Checked-in, Checked-out). Search by guest name. Date range filter. "New Reservation" button. + +**Step 4: Create NewReservation form page** + +Form with: date pickers (check-in/out), room selector (with availability check), guest selector (search existing or create new inline), channel dropdown, adults/children count, notes, total amount. Use react-hook-form + yup validation. + +**Step 5: Add routes in App.jsx** + +```javascript +import Reservations from "./pages/Reservations/Reservations.jsx"; +import NewReservation from "./pages/Reservations/NewReservation.jsx"; + +} /> +} /> +``` + +**Step 6: Commit** + +```bash +git add -A +git commit -m "feat: add reservations UI with card grid and creation form" +``` + +--- + +## Task 9: Guest Management (Backend + Frontend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/guests.controller.js` +- Create: `backend/hotel_hacienda/src/routes/guests.routes.js` +- Create: `frontend/Frontend-Hotel/src/pages/Guests/Guests.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Guests/GuestDetail.jsx` +- Create: `frontend/Frontend-Hotel/src/services/guestService.js` +- Modify: `backend/hotel_hacienda/src/app.js` +- Modify: `frontend/Frontend-Hotel/src/App.jsx` + +**Step 1: Create guests controller** + +- `getGuests` - GET with search (name, email, phone) and pagination (limit/offset) +- `getGuestById` - GET /:id with current stay info (JOIN reservations WHERE status='checked_in') +- `createGuest` - POST with validation +- `updateGuest` - PUT /:id +- `getGuestStays` - GET /:id/stays (JOIN guest_stays + rooms) + +**Step 2: Create guests routes** + +```javascript +router.get('/', ctrl.getGuests); +router.get('/:id', ctrl.getGuestById); +router.post('/', ctrl.createGuest); +router.put('/:id', ctrl.updateGuest); +router.get('/:id/stays', ctrl.getGuestStays); +``` + +**Step 3: Register in app.js** + +```javascript +const guestsRoutes = require('./routes/guests.routes'); +app.use('/api/guests', authMiddleware, guestsRoutes); +``` + +**Step 4: Create guestService.js** + +```javascript +import api from "./api"; +export const getGuests = (params) => api.get("/guests", { params }); +export const getGuestById = (id) => api.get(`/guests/${id}`); +export const createGuest = (data) => api.post("/guests", data); +export const updateGuest = (id, data) => api.put(`/guests/${id}`, data); +export const getGuestStays = (id) => api.get(`/guests/${id}/stays`); +``` + +**Step 5: Create Guests list page** + +Card grid with guest cards (avatar with initials, name, phone, email, current room if checked-in). Search bar. Click to navigate to detail. + +**Step 6: Create GuestDetail page** + +Profile view: personal info, contact, ID info. Stay history table (dates, room, amount, rating). Edit button opens inline form. + +**Step 7: Add routes** + +```javascript +import Guests from "./pages/Guests/Guests.jsx"; +import GuestDetail from "./pages/Guests/GuestDetail.jsx"; + +} /> +} /> +``` + +**Step 8: Commit** + +```bash +git add -A +git commit -m "feat: add guest management with profiles and stay history" +``` + +--- + +## Task 10: Housekeeping Module (Backend + Frontend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/housekeeping.controller.js` +- Create: `backend/hotel_hacienda/src/routes/housekeeping.routes.js` +- Create: `frontend/Frontend-Hotel/src/pages/Housekeeping/Housekeeping.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Housekeeping/components/TaskCard.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Housekeeping/components/StaffPanel.jsx` +- Create: `frontend/Frontend-Hotel/src/services/housekeepingService.js` +- Modify: `backend/hotel_hacienda/src/app.js` +- Modify: `frontend/Frontend-Hotel/src/App.jsx` +- Modify: `frontend/Frontend-Hotel/src/constants/menuconfig.js` + +**Step 1: Create housekeeping controller** + +- `getTasks` - GET with filters: status (pending/in_progress/completed), priority, assigned_to. JOIN rooms for room name, employees for assigned staff name. +- `createTask` - POST (manual task creation) +- `updateTask` - PUT /:id. Handles: + - Assign staff (set assigned_to) + - Start task (set status='in_progress', started_at=NOW()) + - Complete task (set status='completed', completed_at=NOW(), set room status to 'available') +- `getHousekeepingStaff` - GET /staff. Employees in housekeeping department with active/break status. + +**Step 2: Create routes, register in app.js** + +**Step 3: Create frontend service** + +**Step 4: Create Housekeeping page** + +Two-column layout: +- Left: Pending tasks (filterable by priority) +- Right: In-progress tasks +- Bottom or sidebar: Staff availability panel + +Each TaskCard shows: room number, task type badge, priority indicator, assigned staff, action buttons (Assign, Start, Complete based on current status). + +**Step 5: Add to menu config under new "Services" section** + +```javascript +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" }, + ], +}, +``` + +**Step 6: Add route in App.jsx** + +**Step 7: Commit** + +```bash +git add -A +git commit -m "feat: add housekeeping task management with staff assignment" +``` + +--- + +## Task 11: Room Service Module (Backend + Frontend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/roomservice.controller.js` +- Create: `backend/hotel_hacienda/src/routes/roomservice.routes.js` +- Create: `frontend/Frontend-Hotel/src/pages/RoomService/RoomServiceOrders.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/RoomService/MenuManager.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/RoomService/NewOrder.jsx` +- Create: `frontend/Frontend-Hotel/src/services/roomServiceService.js` + +**Step 1: Create roomservice controller** + +- `getOrders` - Active orders (status != 'delivered' AND status != 'cancelled') JOIN rooms, guests. Include order_items + menu_items. +- `createOrder` - POST with items array. Calculate total from item prices. Validate room is occupied. +- `updateOrderStatus` - PUT /:id/status (pending -> preparing -> delivering -> delivered) +- `getMenu` - GET all menu items +- `createMenuItem` - POST +- `updateMenuItem` - PUT /:id (including toggle available) + +**Step 2: Frontend** + +- Orders page: active orders grid with status badges, room number, items list, total, time since created. Status transition buttons. +- Menu manager: table of items with name, price, category, available toggle. Add new item form. +- New order form: select occupied room (dropdown), add items with quantities, notes field. + +**Step 3: Add routes in App.jsx** + +```javascript +} /> +} /> +} /> +``` + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add room service orders and menu management" +``` + +--- + +## Task 12: Events & Venues Module (Backend + Frontend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/events.controller.js` +- Create: `backend/hotel_hacienda/src/routes/events.routes.js` +- Create: `frontend/Frontend-Hotel/src/pages/Events/Venues.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Events/Events.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Events/NewEvent.jsx` +- Create: `frontend/Frontend-Hotel/src/services/eventService.js` + +**Step 1: Create events controller** + +- `getVenues` - GET all venues +- `createVenue` - POST +- `updateVenue` - PUT /:id +- `getEvents` - GET with date filters. JOIN venues. +- `createEvent` - POST. Validate venue availability (no overlapping events). +- `updateEvent` - PUT /:id + +**Step 2: Frontend** + +- Venues page: card grid showing venue name, capacity, area m2, price/hour, amenities tags, status badge. +- Events page: upcoming events list with venue, date/time, organizer, guest count. Filter by date. +- New event form: venue selector, date, start/end time, organizer, guest count, notes. + +**Step 3: Routes and menu config** + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add events and venues management" +``` + +--- + +## Task 13: Employee Scheduling Module (Backend + Frontend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/schedules.controller.js` +- Create: `backend/hotel_hacienda/src/routes/schedules.routes.js` +- Create: `frontend/Frontend-Hotel/src/pages/Schedules/Schedules.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Schedules/components/ShiftCell.jsx` +- Create: `frontend/Frontend-Hotel/src/services/scheduleService.js` + +**Step 1: Create schedules controller** + +- `getSchedules` - GET with params: week_start (date), department. Returns employees with their daily shifts for the week. JOIN employees for name and department. +- `saveSchedules` - POST with array of { employee_id, schedule_date, shift_type }. Uses UPSERT (INSERT ON CONFLICT UPDATE). +- `getEmployeeSchedule` - GET /employee/:id with date range. + +**Step 2: Frontend** + +- Weekly grid: Y-axis = employees (filtered by department), X-axis = Mon-Sun +- Each cell is a ShiftCell: colored by shift type. Click to cycle through: Morning (yellow, 7-15) -> Afternoon (blue, 15-23) -> Night (purple, 23-7) -> Off (gray) -> Morning... +- Department filter dropdown at top +- Save button to bulk-save changes + +**Step 3: Add to menu config under payroll section as "Schedules" or create a new "Staff" section** + +Add to payroll submenu: +```javascript +{ label: "Schedules", spanish_label: "Horarios", route: "/app/schedules" } +``` + +**Step 4: Add route in App.jsx** + +**Step 5: Commit** + +```bash +git add -A +git commit -m "feat: add employee shift scheduling with weekly grid" +``` + +--- + +## Task 14: Operational Reports Module (Backend + Frontend) + +**Files:** +- Create: `backend/hotel_hacienda/src/controllers/operational-reports.controller.js` +- Create: `backend/hotel_hacienda/src/routes/operational-reports.routes.js` +- Create: `frontend/Frontend-Hotel/src/pages/Reports/OperationalReports.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Reports/components/OccupancyChart.jsx` +- Create: `frontend/Frontend-Hotel/src/pages/Reports/components/BookingSourcesPie.jsx` +- Create: `frontend/Frontend-Hotel/src/services/operationalReportService.js` + +**Step 1: Create reports controller** + +- `getOccupancyReport` - Occupancy % by day/week/month. Calculated from room_status_log or daily snapshots. +- `getRevenueReport` - Revenue grouped by room type (from reservations + rooms JOIN). Period: week/month/quarter/year. +- `getBookingSourcesReport` - Count and percentage by channel field in reservations. +- `getSatisfactionReport` - Average rating from guest_stays, grouped by period. + +**Step 2: Frontend** + +- Period selector (Week, Month, Quarter, Year) at the top +- KPI row: occupancy rate with trend, revenue vs target, guest satisfaction stars +- Charts: occupancy trend (line), revenue by room type (bar), booking sources (pie/donut) +- Note: For charts, use simple CSS-based charts or install a lightweight charting library like `recharts` + +**Step 3: Install recharts if needed** + +Run: `cd frontend/Frontend-Hotel && npm install recharts` + +**Step 4: Routes, menu config** + +Add under dashboards or create new Reports section in menu. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "feat: add operational reports with occupancy, revenue, and booking analytics" +``` + +--- + +## Task 15: Update Sidebar Navigation & Final Integration + +**Files:** +- Modify: `frontend/Frontend-Hotel/src/constants/menuconfig.js` (final structure) +- Modify: `frontend/Frontend-Hotel/src/components/Layout2.jsx` (permissions for new sections) +- Modify: `frontend/Frontend-Hotel/src/App.jsx` (verify all routes registered) + +**Step 1: Finalize menu config** + +Update `menuconfig.js` with the complete navigation structure: + +```javascript +export const menuConfig = { + operations: { + label: "Operations", + spanish_label: "Operaciones", + basePath: "/app/room-dashboard", + submenu: [ + { label: "Room Dashboard", spanish_label: "Panel de Habitaciones", route: "/app/room-dashboard" }, + { label: "Reservations", spanish_label: "Reservaciones", route: "/app/reservations" }, + { label: "Guests", spanish_label: "Huespedes", route: "/app/guests" }, + ], + }, + 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" }, + ], + }, + dashboards: { /* existing - unchanged */ }, + income: { /* existing - unchanged */ }, + expensesToApprove: { /* existing - unchanged */ }, + expenses: { /* existing - unchanged */ }, + inventory: { /* existing - unchanged */ }, + payroll: { + /* existing + add Schedules */ + submenu: [ + /* ...existing items... */ + { label: "Schedules", spanish_label: "Horarios", route: "/app/schedules" }, + ], + }, + reports: { + label: "Reports", + spanish_label: "Reportes", + basePath: "/app/operational-reports", + submenu: [ + { label: "Operational", spanish_label: "Operativos", route: "/app/operational-reports" }, + ], + }, + housekeeper: { /* existing - unchanged */ }, +}; +``` + +**Step 2: Update Layout2 permissions** + +Add permission rules for the new sections in `menuConfigWithPermissions`. The new Operations and Services sections should be visible to admin (role 1) and front-desk roles. Adjust based on actual role numbers. + +**Step 3: Verify all routes in App.jsx** + +Ensure all new routes are registered: +- `/app/room-dashboard` +- `/app/reservations`, `/app/reservations/new` +- `/app/guests`, `/app/guests/:id` +- `/app/housekeeping` +- `/app/room-service`, `/app/room-service/menu`, `/app/room-service/new-order` +- `/app/events`, `/app/events/new`, `/app/venues` +- `/app/schedules` +- `/app/operational-reports` + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat: integrate all front-office modules into navigation" +``` + +--- + +## Task 16: Environment & Docker Updates + +**Files:** +- Modify: `backend/hotel_hacienda/.env.example` +- Modify: `docker-compose.yml` + +**Step 1: Update .env.example** + +Add: +``` +JWT_SECRET=change-this-secret-in-production +JWT_REFRESH_SECRET=change-this-refresh-secret-in-production +``` + +**Step 2: Verify Docker setup works with new dependencies** + +Run: `docker compose build` to verify containers build correctly with new npm packages. + +**Step 3: Commit** + +```bash +git add -A +git commit -m "chore: update environment config and Docker build for front-office modules" +``` + +--- + +## Summary + +| Task | Module | Type | +|------|--------|------| +| 1 | Database Schema | Backend | +| 2 | JWT Authentication | Full-stack | +| 3 | i18n Setup | Frontend | +| 4 | Dark Theme | Frontend | +| 5 | Room Dashboard API | Backend | +| 6 | Room Dashboard UI | Frontend | +| 7 | Reservations API | Backend | +| 8 | Reservations UI | Frontend | +| 9 | Guest Management | Full-stack | +| 10 | Housekeeping | Full-stack | +| 11 | Room Service | Full-stack | +| 12 | Events & Venues | Full-stack | +| 13 | Employee Scheduling | Full-stack | +| 14 | Operational Reports | Full-stack | +| 15 | Navigation Integration | Frontend | +| 16 | Environment & Docker | DevOps |