Files
CRM-Hotel/docs/plans/2026-02-15-hotel-front-office-implementation.md
ialcarazsalazar 46b3301b02 Add detailed implementation plan for front-office modules
16 tasks covering: database schema, JWT auth, i18n, dark theme,
room dashboard, reservations, guests, housekeeping, room service,
events/venues, scheduling, reports, navigation, and Docker updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 01:08:12 +00:00

64 KiB

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/<module>.routes.js using express.Router()
  • Controllers: src/controllers/<module>.controller.js with pool.query() calls
  • Frontend pages: src/pages/<Module>/<Page>.jsx
  • Frontend components: src/components/<Component>/<Component>.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

-- 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 <postgres_container> 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

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:

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
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:

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:

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:

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
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 (
    <AuthContext.Provider value={{ user, login, logout, userData, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

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

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:

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:

{
  "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:

{
  "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:

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 (
        <langContext.Provider value={{ lang, toggleLang }}>
            {children}
        </langContext.Provider>
    );
};

export const useLang = () => useContext(langContext);

Step 7: Commit

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:

: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

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:

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:

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:

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:

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:

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

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:

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:

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 <p>{t('common.loading')}</p>;

  return (
    <div>
      <h2 style={{ color: 'var(--text-primary)', marginBottom: 'var(--space-lg)' }}>
        {t('rooms.title')}
      </h2>
      <KPIBar kpis={kpis} />
      <RoomGrid rooms={rooms} onRoomClick={setSelectedRoom} />
      {selectedRoom && (
        <RoomDetailModal
          room={selectedRoom}
          onClose={() => setSelectedRoom(null)}
          onStatusChange={handleStatusChange}
        />
      )}
    </div>
  );
}

Step 6: Add route in App.jsx

Add import and route:

import RoomDashboard from "./pages/RoomDashboard/RoomDashboard.jsx";
// Inside <Route path="/app">:
<Route path="room-dashboard" element={<RoomDashboard />} />

Step 7: Add to menu config

Add to frontend/Frontend-Hotel/src/constants/menuconfig.js:

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

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

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

const reservationsRoutes = require('./routes/reservations.routes');
app.use('/api/reservations', authMiddleware, reservationsRoutes);

Step 4: Commit

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

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

import Reservations from "./pages/Reservations/Reservations.jsx";
import NewReservation from "./pages/Reservations/NewReservation.jsx";

<Route path="reservations" element={<Reservations />} />
<Route path="reservations/new" element={<NewReservation />} />

Step 6: Commit

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

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

const guestsRoutes = require('./routes/guests.routes');
app.use('/api/guests', authMiddleware, guestsRoutes);

Step 4: Create guestService.js

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

import Guests from "./pages/Guests/Guests.jsx";
import GuestDetail from "./pages/Guests/GuestDetail.jsx";

<Route path="guests" element={<Guests />} />
<Route path="guests/:id" element={<GuestDetail />} />

Step 8: Commit

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

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

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

<Route path="room-service" element={<RoomServiceOrders />} />
<Route path="room-service/menu" element={<MenuManager />} />
<Route path="room-service/new-order" element={<NewOrder />} />

Step 4: Commit

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

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:

{ label: "Schedules", spanish_label: "Horarios", route: "/app/schedules" }

Step 4: Add route in App.jsx

Step 5: Commit

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

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:

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

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

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