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

2151 lines
64 KiB
Markdown

# 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**
```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 <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**
```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 (
<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**
```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 (
<langContext.Provider value={{ lang, toggleLang }}>
{children}
</langContext.Provider>
);
};
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 <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:
```javascript
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`:
```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";
<Route path="reservations" element={<Reservations />} />
<Route path="reservations/new" element={<NewReservation />} />
```
**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";
<Route path="guests" element={<Guests />} />
<Route path="guests/:id" element={<GuestDetail />} />
```
**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
<Route path="room-service" element={<RoomServiceOrders />} />
<Route path="room-service/menu" element={<MenuManager />} />
<Route path="room-service/new-order" element={<NewOrder />} />
```
**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 |