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>
2151 lines
64 KiB
Markdown
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 |
|