- README.md con guía completa de instalación y arquitectura - docs/BACKEND.md — guía detallada del API - docs/FRONTEND.md — guía del SPA React - docs/DATABASE.md — funciones SQL y patrones de datos - docs/API.md — referencia completa de endpoints REST - docs/DEPLOYMENT.md — guía de despliegue en producción - docs/TROUBLESHOOTING.md — solución de problemas comunes - CHANGELOG.md — historial de cambios - CONTRIBUTING.md — guía de contribución
14 KiB
🎨 Documentación del Frontend — Frontend-Hotel
Índice
- Introducción
- Estructura de Carpetas
- Entry Points
- Enrutamiento
- Manejo de Estado Global
- Sistema de Permisos (Roles)
- Comunicación con el Backend
- Componentes Reutilizables
- Estilos
- Páginas por Dominio
- Guía para Agregar una Nueva Página
- Bugs y Limitaciones Conocidas
Introducción
El frontend es una Single Page Application (SPA) construida con React 19, Vite y React Router DOM. Es una aplicación cliente que consume la API REST del backend.
Tecnologías clave:
- React 19.1.1 (JSX, JavaScript puro, sin TypeScript)
- Vite 7.1.2 con SWC para compilación rápida
- React Router DOM 7.8.2 (BrowserRouter)
- Estado global: React Context API
- HTTP: mezcla de
fetchnativo yaxios - Estilos: Tailwind CSS + Bootstrap 5 + CSS puro
Estructura de Carpetas
Frontend-Hotel/
├── index.html # Entry HTML (título: "Hacienda San Angel")
├── vite.config.js # Config Vite (puerto 5172)
├── tailwind.config.cjs # Config Tailwind
├── postcss-config.js # PostCSS con Tailwind + Autoprefixer
├── eslint.config.js # ESLint 9 flat config
├── public/
│ ├── logoHotel.png
│ ├── IconoHotel.svg
│ └── ...
├── src/
│ ├── main.jsx # Entry point React
│ ├── App.jsx # Definición central de rutas
│ ├── index.css # Tailwind directives + estilos base
│ ├── App.css
│ ├── constants/
│ │ └── menuconfig.js # Configuración del menú de navegación
│ ├── context/
│ │ ├── AuthContext.jsx # Estado de autenticación
│ │ └── LenguageContext.jsx # Estado de idioma (EN/ES)
│ ├── routes/
│ │ └── ProtectedRoute.jsx # Ruta protegida (NO usada en App.jsx)
│ ├── services/
│ │ ├── api.js # Instancia axios (baseURL desde env)
│ │ ├── employeeService.js
│ │ ├── incomeService.js
│ │ ├── userService.js
│ │ └── contractService.js
│ ├── styles/
│ │ ├── global.css
│ │ ├── Dashboard.css
│ │ ├── Login.css
│ │ ├── Layout.css
│ │ └── Sidebar.css
│ ├── assets/
│ │ └── pages/login.jsx
│ ├── components/
│ │ ├── Layout2.jsx # Layout activo (Sidebar + Topbar + Outlet)
│ │ ├── Layout.jsx # Layout antiguo (comentado)
│ │ ├── Sidebar.jsx
│ │ ├── topbar/Topbar.jsx
│ │ ├── Navbar/Navbar.jsx
│ │ ├── Table/HotelTable.jsx
│ │ ├── ExcelExportButton.jsx
│ │ ├── SummaryCard.jsx
│ │ ├── FormInput.jsx
│ │ ├── FormSelect.jsx
│ │ ├── Switch.jsx
│ │ ├── Modals/
│ │ ├── Buttons/
│ │ ├── Filters/
│ │ └── Inputs/
│ └── pages/ # ~50+ páginas organizadas por dominio
│ ├── Login.jsx
│ ├── Dashboard.jsx
│ ├── Dashboard/
│ ├── Expenses/
│ ├── ExpensesToBeApproval/
│ ├── Inventory/
│ ├── Hotel/
│ ├── Income/
│ ├── Payroll/
│ ├── Settings/
│ └── ...
Entry Points
index.html
Carga la fuente Roboto desde Google Fonts y el script src/main.jsx.
src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import { LangProvider } from "./context/LenguageContext";
import App from "./App.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(
<AuthProvider>
<LangProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</LangProvider>
</AuthProvider>
);
Jerarquía de providers:
AuthProvider— disponible en toda la appLangProvider— depende deAuthContext(lee el rol para forzar español a housekeepers)BrowserRouter— enrutamiento
Enrutamiento
src/App.jsx
Define todas las rutas de la aplicación. No usa ProtectedRoute. Todas las rutas internas están bajo /app y renderizan <Layout /> (que es Layout2.jsx).
<Route path="/" element={<Login />} />
<Route path="/app" element={<Layout />}>
<Route path="income" element={<Income/>} />
<Route path="employees" element={<Employees />} />
<Route path="contracts" element={<Contracts />} />
<Route path="hotelpl" element={<HotelPL />} />
<Route path="restaurantpl" element={<RestaurantPL />} />
<Route path="expenses" element={<Expenses />} />
<Route path="new-expense" element={<NewExpense />} />
<Route path="products" element={<Products />} />
<Route path="payroll" element={<Payroll />} />
{/* ... ~40 rutas más */}
</Route>
Convenciones de rutas:
- Páginas de listado: plural simple (
/products,/payroll) - Páginas de creación: prefijo
new-(/new-product,/new-expense) - Páginas de edición:
:idcomo parámetro (/expenses/edit/:id,/payroll/employee/:id) - Detalles: segmento fijo +
:id(/expenses/:id,/properties/:id)
Manejo de Estado Global
AuthContext (src/context/AuthContext.jsx)
export const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null); // rol numérico
const [userData, setUserData] = useState(null);
useEffect(() => {
const savedUser = localStorage.getItem("rol");
if (savedUser) setUser(JSON.parse(savedUser));
}, []);
const login = (userData, data) => {
setUserData(data);
setUser(userData);
localStorage.setItem("rol", JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem("rol");
};
return (
<AuthContext.Provider value={{ user, login, logout, userData }}>
{children}
</AuthContext.Provider>
);
}
Observaciones:
- Guarda el rol como número en
localStoragebajo la clave"rol". - NO exporta
useAuth, lo cual es un bug conocido. - No usa JWT ni tokens.
LangContext (src/context/LenguageContext.jsx)
export const langContext = createContext();
export const LangProvider = ({ children }) => {
const authContext = useContext(AuthContext);
const user = authContext?.user || null;
const [lang, setLang] = useState(user === 6 ? "es" : "en");
const toggleLang = (event) => setLang(event.target.value);
return (
<langContext.Provider value={{ lang, toggleLang }}>
{children}
</langContext.Provider>
);
};
export const useLang = () => useContext(langContext);
Observaciones:
- Forza español si
user === 6(Housekeeper). - No persiste en localStorage.
- Se accede con
useContext(langContext)ouseLang().
Sistema de Permisos (Roles)
Los permisos son numéricos y hardcodeados en Layout2.jsx. No hay backend de ACL.
Roles Definidos
| Rol | Acceso |
|---|---|
| 1 | Admin — Todo |
| 2 | Supervisor limitado — Dashboards, Expenses (Report/Monthly Report), Payroll (Report/Attendance/Employees/Contracts), Expenses to be approved |
| 3-4 | Rangos intermedios (según lógica de rangos) |
| 5 | Compras — Solo: New Expense, Purchase Entries, New Suppliers |
| 6 | Housekeeper — Solo Outcomes, forzado a español |
Lógica de Permisos en Layout2.jsx
const menuConfigWithPermissions = Object.values(menuConfig).map(section => ({
...section,
hidden:
section.label === "Dashboards" ? (user >= 1 && user <= 2 ? false : true) :
section.label === "Expenses to be approved" ? (user === 1 || user === 2 ? false : true) :
section.label === "Expenses" ? (user >= 1 && user <= 5 ? false : true) :
section.label === "Inventory" ? (user >= 1 && user <= 5 ? false : true) :
section.label === "Payroll" ? (user >= 1 && user <= 4 ? false : true) :
section.label === "Hotel" ? (user === 1 ? false : true) :
section.label === "Income" ? (user >= 1 && user <= 4 ? false : true) :
section.label === "Housekeeper" ? (user === 6 ? false : true) :
false,
// ... lógica de submenús
}));
Regla: Si hidden === true, la sección no aparece en el sidebar.
Comunicación con el Backend
Variable de Entorno
VITE_API_BASE_URL=http://localhost:4000/api
Se accede en el frontend como:
import.meta.env.VITE_API_BASE_URL
Instancia Axios (src/services/api.js)
import axios from "axios";
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
});
export default api;
Problema: Esta instancia está infrautilizada. Muchas páginas usan fetch directo o axios sin la instancia base.
Patrón Fetch (más común)
fetch(import.meta.env.VITE_API_BASE_URL + '/purchases/getpurchases')
.then(res => res.json())
.then(data => setState(data))
.catch(err => console.error(err));
Patrón Axios
axios.put(`${import.meta.env.VITE_API_BASE_URL}/purchases/entry/${id}`, {
checking: parseInt(checking)
})
.then(res => { ... })
.catch(err => { ... });
Problemas conocidos:
- Varios archivos en
services/tienen URLs hardcodeadas alocalhost:3000olocalhost:4000. - No hay interceptores para manejo centralizado de errores.
- No se envían headers de autorización.
Componentes Reutilizables
HotelTable.jsx (src/components/Table/)
Tabla genérica que recibe:
columns: array de{ header, key, render?, headerStyle? }data: array de objetos
<Table
columns={[
{ header: "Nombre", key: "name" },
{ header: "Acciones", key: "id", render: (id) => <button>Editar</button> }
]}
data={products}
/>
ExcelExportButton.jsx
Exporta datos a .xlsx usando la librería xlsx.
SummaryCard.jsx
Card de resumen con indicador de loading.
FormInput.jsx / FormSelect.jsx
Wrappers básicos de inputs HTML con estilos consistentes.
Modales (src/components/Modals/)
ConfirmationModal.jsx— Confirmación genéricaDiscardConfirmModal.jsx— Confirmación de descarteConfirmationMontlyPay.jsx— Confirmación de pago mensualConfirmationOutcome.jsx— Confirmación de salida
Estilos
Mezcla de Tecnologías
El proyecto usa tres sistemas de estilos simultáneamente:
- Tailwind CSS — Configurado pero usado esporádicamente.
- Bootstrap 5 — Clases como
btn btn-primary,btn btn-secondary. - CSS Puro — Un archivo
.csspor página/componente.
Convención Actual
Cada página tiene su propio archivo CSS:
src/pages/Expenses/NewExpense.css
src/pages/Inventory/Products.css
src/components/Table/Table.css
Fuente
Roboto cargada desde Google Fonts en index.html.
Páginas por Dominio
| Dominio | Páginas | Ruta Base |
|---|---|---|
| Dashboards | Income, HotelPL, RestaurantPL, RoomAnalysis, RestaurantAnalysis, Budget, CostPerRoom, Expenses | /app/income, /app/hotelpl... |
| Income | NewIncome, IncomeReport | /app/new-income-form, /app/new-income-report |
| Expenses to Approve | PendingApproval, Approved, Rejected | /app/pending-approval... |
| Expenses | NewExpense, EditExpense, ExpenseDetail, ReportExpense, Payments, MonthlyPayments, MonthlyReport, NewMonthlyPayment, NewSuppliers, PurchaseEntries | /app/new-expense... |
| Inventory | Products, NewProduct, AlterProduct, InventoryReport, Adjustments, Outcomes, HousekeeperOutcomes, DiscardProduct | /app/products... |
| Hotel | Properties, PropertiesId | /app/properties... |
| Payroll | Payroll, Plantillapayroll, EditPayroll, PayrollEmployees, NewEmployee, PayrollAttendance, PayrollContract, ContractsDetail | /app/payroll... |
| Settings | Settings, SettingsId, RoomsManagement | /app/settings... |
Guía para Agregar una Nueva Página
- Crear el componente en
src/pages/[Dominio]/NombrePagina.jsx. - Crear los estilos opcionales en
src/pages/[Dominio]/NombrePagina.css. - Agregar la ruta en
src/App.jsxdentro de<Route path="/app" element={<Layout />}>. - Agregar al menú (si aplica):
- Editar
src/constants/menuconfig.js - Agregar la ruta a la sección correspondiente
- Editar
- Actualizar permisos en
src/components/Layout2.jsx:- Agregar lógica de
hiddenpara la sección/submenú - Agregar detección de ruta en
activeSectionsi la ruta no sigue el patrón estándar
- Agregar lógica de
- Agregar traducciones EN/ES si es texto visible.
- Consumir la API usando
import.meta.env.VITE_API_BASE_URL.
Bugs y Limitaciones Conocidas
useAuthno existe —AuthContext.jsxno exportauseAuth, peroProtectedRoute.jsxyNavbar.jsxlo intentan importar.- No hay rutas protegidas —
App.jsxno usaProtectedRoute. Cualquiera puede acceder a/app/*. - URLs hardcodeadas — Varios
*Service.jsapuntan alocalhosten vez de usarVITE_API_BASE_URL. - Código comentado masivo — Especialmente en
Layout.jsxy páginas grandes. multeren frontend — Es un middleware de Node.js, inapropiado para React.- Sin tests — No hay Jest, Vitest, ni Playwright configurados.