docs: documentación extensiva del proyecto

- 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
This commit is contained in:
Consultoria AS
2026-06-09 16:26:09 -07:00
parent f284a5ca27
commit 411d517ba4
9 changed files with 2728 additions and 295 deletions

699
docs/API.md Normal file
View File

@@ -0,0 +1,699 @@
# 🔌 Referencia Completa de la API REST
**Base URL:** `https://<dominio>/api`
---
## Índice
1. [Convenciones](#convenciones)
2. [Autenticación](#autenticación)
3. [Empleados](#empleados)
4. [Contratos](#contratos)
5. [Productos](#productos)
6. [Gastos](#gastos)
7. [Aprobaciones y Pagos](#aprobaciones-y-pagos)
8. [Pagos Mensuales](#pagos-mensuales)
9. [Configuraciones](#configuraciones)
10. [Emails](#emails)
11. [Ingresos (Little Hotelier)](#ingresos-little-hotelier)
12. [Compras](#compras)
13. [Tipo de Cambio](#tipo-de-cambio)
14. [Hotel P&L](#hotel-pl)
15. [Restaurant P&L](#restaurant-pl)
16. [Ingresos Horux](#ingresos-horux)
---
## Convenciones
- **Formato:** JSON en request body y response.
- **Paginación:** `?page=N&limit=M` (default limit: 500 para empleados, 10 para productos).
- **Errores:** Status `500` con `{ message: "Error descripción" }`.
- **CORS:** Origen controlado por `URL_CORS` en el backend.
---
## Autenticación
> ⚠️ **Nota:** No hay JWT. El login devuelve un rol numérico que el frontend guarda en `localStorage`.
### POST `/api/auth/login`
```json
// Request
{
"name_mail_user": "usuario_o_email",
"user_pass": "contraseña"
}
// Response 200
{
"rol": 1,
"user_id": 5,
"user_name": "Admin",
"message": "Usuario autenticado correctamente"
}
```
**Status codes de la función SQL:**
- `status: 1` = Autenticado
- `status: 2` = Credenciales incorrectas
### POST `/api/auth/createuser`
```json
// Request
{
"name_user": "Nuevo Usuario",
"id_rol": 2,
"email": "user@email.com",
"user_pass": "password123"
}
// Response
{
"message": "Se agrego el usuario correctamente",
"status": 1
}
```
### POST `/api/auth/recoverpass`
Genera una contraseña aleatoria y la envía por correo.
```json
// Request
{ "user_mail": "user@email.com" }
// Response
{
"message": "Correo enviado con nueva contraseña.",
"status": 1
}
```
---
## Empleados
Base: `/api/employees`
### GET `/api/employees/`
Lista paginada de empleados.
**Query params:** `?page=1&limit=500`
```json
// Response
{
"page": 1,
"limit": 500,
"total": 45,
"totalPages": 1,
"data": [
{
"employee_rfc": "RFC123456",
"name_employee": "Juan Pérez",
"nss_employe": "12345678901",
"position_employee": "Recepcionista",
"area_employee": "Front Desk",
"phone_employee": "5551234567",
"end_contract": "2025-12-31",
"daily_pay": 500.00,
"uniforms": 2,
"status": "Activo",
"birthday": "1990-05-15",
"curp": "CURP123456HDF"
}
]
}
```
### GET `/api/employees/activeEmployees`
```json
{ "message": "Total de empleados activos", "data": 42 }
```
### GET `/api/employees/getattendance`
```json
{ "message": "attendance", "data": [...] }
```
### GET `/api/employees/gradeofstudy`
Catálogo de grados de estudio.
### GET `/api/employees/relationship`
Catálogo de parentescos.
### POST `/api/employees/employee`
Obtiene un empleado por RFC.
```json
// Request
{ "rfcEmployee": "RFC123456" }
```
### POST `/api/employees/newemployee`
```json
// Request
{
"name_emp": "Juan Pérez",
"rfc_emp": "RFC123456",
"nss_emp": "12345678901",
"addres_emp": "Calle 123",
"phone_emp": "5551234567",
"email_emp": "juan@email.com",
"birthday_emp": "1990-05-15",
"curp_emp": "CURP123456HDF",
"study_emp": 1,
"emergency_name": "María Pérez",
"emergency_tel": "5559876543",
"relationship_id": 1
}
```
### POST `/api/employees/updateemployee`
Misma firma que `newemployee`. Actualiza por RFC.
---
## Contratos
Base: `/api/contracts`
### GET `/api/contracts/`
### GET `/api/contracts/getinfocontract/:id`
### GET `/api/contracts/neartoend`
### GET `/api/contracts/positions`
### GET `/api/contracts/areas`
### GET `/api/contracts/bosses`
### GET `/api/contracts/reportempcontract`
### GET `/api/contracts/disabledcontract`
### POST `/api/contracts/newcontract`
### PUT `/api/contracts/updatecontract/:id`
---
## Productos
Base: `/api/products`
### GET `/api/products/`
Paginado (default limit: 10).
### GET `/api/products/productcategory`
### GET `/api/products/producttype`
### GET `/api/products/suppliers`
### GET `/api/products/gdiscardproducts`
### GET `/api/products/reportinventory`
### GET `/api/products/stockadjusment`
### GET `/api/products/gethousekeeper`
### GET `/api/products/getproducts`
### GET `/api/products/getconsumptionstockreport`
### POST `/api/products/newsupplier`
```json
{
"new_name_supp": "Proveedor SA",
"new_rfc_supp": "RFC123456",
"new_mail_supp": "prov@email.com",
"new_phone_supp": "5551234567"
}
```
### POST `/api/products/newproduct`
```json
{
"new_name_product": "Toallas",
"new_sku_product": "TOW-001",
"product_type": [{"id": 1, "name": "Textil"}],
"new_category": 1,
"suppliers_id": 1,
"unit": 1,
"new_stock": 100,
"newprice_product": 150.00,
"new_tax": 1,
"currency": 1,
"image_product": "url_o_base64"
}
```
> Nota: `product_type` se serializa a JSONB.
### POST `/api/products/stockadjusmentset`
```json
{
"stockproductadjusment": [
{ "id_product": 1, "new_stock": 50 },
{ "id_product": 2, "new_stock": 30 }
]
}
```
### POST `/api/products/newconsumptionstock`
```json
{
"product_id": 1,
"quantity_consumption": 5,
"date_consumption": "2025-01-15",
"rfc_emp": "RFC123456"
}
```
### POST `/api/products/disableSupplier`
```json
{ "supplier_id": 1 }
```
### PUT `/api/products/update_product/:id`
### PUT `/api/products/discardproduct/:id`
```json
{
"quantity": 5,
"reasons": "Dañado"
}
```
### PUT `/api/products/product/:id`
### PUT `/api/products/updatesupplier/:id`
⚠️ **Bug conocido:** Falta `/` inicial en la ruta del backend.
---
## Gastos
Base: `/api/expenses`
### GET `/api/expenses/pendingapproval`
### GET `/api/expenses/approvedexpenses`
### GET `/api/expenses/rejectedexpenses`
### GET `/api/expenses/mainsupplier`
### PUT `/api/expenses/getexpense/:id`
⚠️ **Nota:** Es `PUT` pero debería ser `GET`. No requiere body.
### GET `/api/expenses/getinfo`
Devuelve catálogos combinados (suppliers, categories, areas, users, currencies, uniforms, taxes).
### GET `/api/expenses/reportexpenses`
### POST `/api/expenses/countpending`
```json
{ "option": 1 } // 1 = aprobaciones, 2 = mensuales
```
### GET `/api/expenses/reportpayments`
### GET `/api/expenses/monthlypayments`
### POST `/api/expenses/newexpense`
```json
{
"new_description": "Compra de amenities",
"suppliers_id": 1,
"new_request_date": "2025-01-01",
"new_payment_deadline": "2025-01-31",
"request_by": 1,
"area": 1,
"expense_cat": 1,
"currency_id": 1,
"products": [
{ "id_product": 1, "quantity": 10 },
{ "id_product": 2, "quantity": 5 }
],
"new_iva": 16.00,
"new_ieps": 0.00,
"new_subtotal": 1000.00,
"new_total": 1160.00,
"needtoapprove": true
}
```
### POST `/api/expenses/totalapproved`
```json
{ "option": 1 } // 1 = aprobado, 2 = rechazado
```
### PUT `/api/expenses/updateexpense/:id`
Misma estructura que `newexpense`, con campos prefijados con `up_`.
### GET `/api/expenses/gettaxes`
---
## Aprobaciones y Pagos
Base: `/api/status`
### PUT `/api/status/approveupdate/:id`
```json
{
"status": 2,
"approved_by": 1
}
```
**Status:**
- `2` = Aprobado
- `3` = Rechazado
### PUT `/api/status/paymentupdate/:id`
```json
{ "status": 2 }
```
### GET `/api/status/penapppayments`
### GET `/api/status/countdelaypay`
### GET `/api/status/totalspent`
---
## Pagos Mensuales
Base: `/api/payment`
### POST `/api/payment/newexpmonthly`
```json
{
"descriptionex": "Renta",
"recurrence_id": 1,
"payment_type": 1,
"currency_id": 1,
"suppliers_id": 1,
"area": 1,
"expense_category": 1,
"day_expense": 15,
"tax_id": 1,
"new_subtotal": 5000.00
}
```
### GET `/api/payment/refreshmonthly`
### PUT `/api/payment/paymentstatusmonthly/:id`
```json
{
"status_payment": 2,
"tax_id": 1,
"subtotal": 5000.00
}
```
Si `subtotal` es null, solo actualiza el estado sin recalcular total.
### PUT `/api/payment/updateexpmonthly/:id`
### GET `/api/payment/onemothlyexpense/:id`
### PUT `/api/payment/needtorefresh/:id`
```json
{ "notrefresh": true }
```
---
## Configuraciones
Base: `/api/settings`
### POST `/api/settings/newroom`
### POST `/api/settings/newproperty`
### GET `/api/settings/reportrooms`
### GET `/api/settings/reportproperties`
### GET `/api/settings/approveby`
### GET `/api/settings/requestby`
### GET `/api/settings/categoryexpense`
### GET `/api/settings/currency`
### GET `/api/settings/units`
### GET `/api/settings/recurrence`
---
## Emails
Base: `/api/emails`
> ⚠️ **Nota:** Todos estos endpoints tienen validación de frecuencia (1 ejecución por mes) mediante la función `validateendpoint()`.
### POST `/api/emails/nearexpiring`
### POST `/api/emails/paymentdelay`
### POST `/api/emails/expensesneartodeadline`
### POST `/api/emails/expensesspecial`
### POST `/api/emails/birthdays`
### POST `/api/emails/contractexpired`
### POST `/api/emails/expiredcontractsmonth`
---
## Ingresos (Little Hotelier)
Base: `/api/incomes`
> **Nota:** La mayoría de estos endpoints reciben filtros de fecha en el body.
### POST `/api/incomes/getincomes`
### POST `/api/incomes/totalincomes`
### POST `/api/incomes/channelscards`
### POST `/api/incomes/loadincomes`
Carga masiva desde CSV/XLSX previamente colocado en `src/resources/littleHotelier/`.
### POST `/api/incomes/loadproductsales`
### POST `/api/incomes/loadchequesdetalle`
### POST `/api/incomes/reportincomes`
### POST `/api/incomes/countticket`
### POST `/api/incomes/efectivo`
### POST `/api/incomes/otros`
### POST `/api/incomes/propinas`
### POST `/api/incomes/tarjeta`
### POST `/api/incomes/vales`
### POST `/api/incomes/sumatotal`
### POST `/api/incomes/ticketpromedio`
### GET `/api/incomes/getproductsales`
### GET `/api/incomes/getdetallecheque`
---
## Compras
Base: `/api/purchases`
### GET `/api/purchases/getpurchases`
Devuelve líneas de compra pendientes de recepción.
```json
// Response
{
"message": "Se obtuvieron todos los Purchases details",
"data": [
{
"id_purchase_dt": 1,
"id_expense": 5,
"id_product": 2,
"product_name": "Toallas",
"quantity": 10,
"delivered": 0,
"id_tax": 1,
"total": 1500.00
}
]
}
```
### PUT `/api/purchases/entry/:id`
Registra la recepción física de una compra.
```json
// Request
{ "checking": 10 }
// Response
{
"message": "Se obtuvo el Purchases details",
"newentry": 1
}
```
---
## Tipo de Cambio
Base: `/api/exchange`
### POST `/api/exchange/consultexchange`
Consulta tipo de cambio actual desde API de Banxico.
### GET `/api/exchange/getexchanges`
Devuelve historial de tipos de cambio guardados.
---
## Hotel P&L
Base: `/api/hotelpl`
### POST `/api/hotelpl/cogs`
### POST `/api/hotelpl/ebitda`
### POST `/api/hotelpl/employeeshare`
### POST `/api/hotelpl/grossprofit`
### POST `/api/hotelpl/tips`
### POST `/api/hotelpl/totalrevenue`
### POST `/api/hotelpl/weightedCategoriesCost`
---
## Restaurant P&L
Base: `/api/restaurantpl`
### POST `/api/restaurantpl/cogs`
### POST `/api/restaurantpl/ebitda`
### POST `/api/restaurantpl/grossprofit`
### POST `/api/restaurantpl/totalrevenue`
### POST `/api/restaurantpl/weightedCategoriesCost`
---
## Ingresos Horux
Base: `/api/incomeshrx`
### GET `/api/incomeshrx/accountincome`
### GET `/api/incomeshrx/categoryincome`
### GET `/api/incomeshrx/invoiceIncome`
### GET `/api/incomeshrx/totalIncome`
### GET `/api/incomeshrx/incomehorux`
### GET `/api/incomeshrx/oneincomehorux/:id`
### GET `/api/incomeshrx/stripedata/`
Obtiene transfers de Stripe y las inserta en PostgreSQL.
### POST `/api/incomeshrx/stripedatademo/`
Crea un transfer de demo en Stripe.
### POST `/api/incomeshrx/insertinvoice`
Descarga facturas de API externa (año actual) y las inserta.
### POST `/api/incomeshrx/newincome`
```json
{
"account_id": 1,
"amount": 5000.00,
"new_date": "2025-01-15",
"newinvoice": "FAC-001",
"area_id": 1,
"categories": [{"id": 1, "amount": 5000}]
}
```
### PUT `/api/incomeshrx/updateincome/:id`
Misma estructura que `newincome`.
---
> **Fin de la referencia API.** Para ver la lógica interna de cada endpoint, revisa los controllers en `backend/hotel_hacienda/src/controllers/`.

365
docs/BACKEND.md Normal file
View File

@@ -0,0 +1,365 @@
# 📦 Documentación del Backend — API Hotel Hacienda
## Índice
1. [Introducción](#introducción)
2. [Estructura de Carpetas](#estructura-de-carpetas)
3. [Flujo de una Petición](#flujo-de-una-petición)
4. [Configuración de Express (`app.js`)](#configuración-de-express-appjs)
5. [Conexión a Base de Datos](#conexión-a-base-de-datos)
6. [Controllers](#controllers)
7. [Rutas](#rutas)
8. [Middlewares](#middlewares)
9. [Servicios](#servicios)
10. [Variables de Entorno](#variables-de-entorno)
11. [Patrones y Convenciones](#patrones-y-convenciones)
12. [Guía para Agregar un Nuevo Endpoint](#guía-para-agregar-un-nuevo-endpoint)
---
## Introducción
El backend es una API REST construida con **Node.js** y **Express.js**. No utiliza ORM; la lógica de negocio reside en **funciones SQL de PostgreSQL** que son invocadas desde los controllers.
**Entry point:** `index.js``src/server.js``src/app.js`
---
## Estructura de Carpetas
```
hotel_hacienda/
├── index.js # Solo requiere src/server.js
├── src/
│ ├── server.js # Levanta el servidor
│ ├── app.js # Configura Express, CORS, rutas
│ ├── db/
│ │ └── connection.js # Pool de PostgreSQL
│ ├── middlewares/
│ │ ├── handleValidation.js # Manejo de errores de express-validator
│ │ └── validators.js # Validadores (login)
│ ├── routes/ # 16 archivos de rutas
│ │ ├── auth.routes.js
│ │ ├── employee.routes.js
│ │ ├── contract.routes.js
│ │ ├── product.routes.js
│ │ ├── expense.routes.js
│ │ ├── status.routes.js
│ │ ├── payment.routes.js
│ │ ├── settings.routes.js
│ │ ├── mail.routes.js
│ │ ├── incomes.routes.js
│ │ ├── purchase.routes.js
│ │ ├── exchange.routes.js
│ │ ├── hotelpl.routes.js
│ │ ├── restaurantpl.routes.js
│ │ ├── incomehrx.routes.js
│ │ └── reportcontract.routes.js
│ ├── controllers/ # 18 archivos de controllers
│ │ ├── auth.controller.js
│ │ ├── employee.controller.js
│ │ ├── contract.controller.js
│ │ ├── product.controller.js
│ │ ├── expense.controller.js
│ │ ├── status.controller.js
│ │ ├── payment.controller.js
│ │ ├── settings.controller.js
│ │ ├── mail.controller.js
│ │ ├── incomes.controller.js
│ │ ├── purchase.controller.js
│ │ ├── exchange.controller.js
│ │ ├── hotelp.controller.js
│ │ ├── restaurantpl.controller.js
│ │ ├── incomehrx.controller.js
│ │ ├── reportcontract.controller.js
│ │ └── reporter.controller.js
│ └── services/
│ └── mailService.js # Transporte de Nodemailer
```
---
## Flujo de una Petición
```
HTTP Request
┌─────────────┐
│ Express │ ← CORS, JSON parser
│ (app.js) │
└──────┬──────┘
┌─────────────┐
│ Routes │ ← Prefijo /api/xxx
│ (.routes) │
└──────┬──────┘
┌─────────────┐
│ Controllers │ ← Extrae req.params/body
│ (.controller)│
└──────┬──────┘
┌─────────────┐
│ Pool │ ← pg (node-postgres)
│ (connection)│
└──────┬──────┘
┌─────────────┐
│ PostgreSQL │ ← Funciones SQL
│ (funciones)│
└─────────────┘
```
---
## Configuración de Express (`app.js`)
```javascript
const express = require('express');
const app = express();
const cors = require('cors');
require("dotenv").config();
const corsOptions = {
origin: process.env.URL_CORS,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"]
};
app.use(cors(corsOptions));
app.use(express.json());
```
**Observaciones:**
- `URL_CORS` debe definirse sin slash al final.
- No hay middleware de autenticación global.
- Todas las rutas usan prefijo `/api/`.
---
## Conexión a Base de Datos
```javascript
// src/db/connection.js
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
pool.connect()
.then(() => console.log('Conectado a PostgreSQL'))
.catch(err => console.error('Error de conexión:', err));
module.exports = pool;
```
**Nota:** Se usa un `Pool` reutilizable. Cada controller lo importa directamente.
---
## Controllers
### Patrón General
```javascript
const pool = require('../db/connection');
const nombreFuncion = async (req, res) => {
try {
const { param1, param2 } = req.body; // o req.params / req.query
const result = await pool.query(
'SELECT nombrefuncionsql($1, $2) AS status',
[param1, param2]
);
const status = result.rows[0].status;
res.json({ message: 'Éxito', status });
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Error interno' });
}
};
module.exports = { nombreFuncion };
```
### Paginación Manual
Algunos endpoints implementan paginación manual:
```javascript
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 500;
const offset = (page - 1) * limit;
const result = await pool.query(
'SELECT * FROM getemployees() LIMIT $1 OFFSET $2',
[limit, offset]
);
const totalResult = await pool.query('SELECT COUNT(*) FROM employees');
const total = parseInt(totalResult.rows[0].count);
const totalPages = Math.ceil(total / limit);
res.json({ page, limit, total, totalPages, data: result.rows });
```
### Manejo de JSONB
Para pasar arrays/objetos a PostgreSQL:
```javascript
const categoriesJson = categories ? JSON.stringify(categories) : '[]';
await pool.query(
'SELECT newincome($1,$2,$3,$4,$5,$6::jsonb) AS status',
[param1, param2, param3, param4, param5, categoriesJson]
);
```
---
## Rutas
### Convención de Nombres
- Archivos: `nombreplural.routes.js`
- Prefijo en `app.js`: `/api/nombreplural`
### Ejemplo
```javascript
// src/routes/expense.routes.js
const express = require('express');
const router = express.Router();
const expenseController = require('../controllers/expense.controller');
router.get('/pendingapproval', expenseController.getPendingAppExpenses);
router.post('/newexpense', expenseController.newExpense);
router.put('/updateexpense/:id', expenseController.updateExpense);
module.exports = router;
```
### Registro en `app.js`
```javascript
const expenseRoutes = require('./routes/expense.routes');
app.use('/api/expenses', expenseRoutes);
```
---
## Middlewares
### Validación de Login
```javascript
// src/middlewares/validators.js
const { body } = require('express-validator');
const loginValidator = [
body('name_mail_user').notEmpty().withMessage('Usuario requerido'),
body('user_pass').notEmpty().withMessage('Contraseña requerida')
];
module.exports = { loginValidator };
```
### Manejo de Errores de Validación
```javascript
// src/middlewares/handleValidation.js
const { validationResult } = require('express-validator');
const handleValidation = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
module.exports = handleValidation;
```
**Nota:** Actualmente solo el login tiene validación. Los demás endpoints no validan inputs.
---
## Servicios
### Mail Service (`src/services/mailService.js`)
Configura un transporte SMTP reutilizable con Nodemailer:
```javascript
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
module.exports = transporter;
```
**Uso:** Importado en controllers que envían correos (`auth.controller.js`, `mail.controller.js`).
---
## Variables de Entorno
| Variable | Descripción | Requerida |
|----------|-------------|-----------|
| `PORT` | Puerto del servidor | Sí (default 3000) |
| `URL_CORS` | Origen permitido para CORS | Sí |
| `DB_HOST` | Host de PostgreSQL | Sí |
| `DB_PORT` | Puerto de PostgreSQL | Sí |
| `DB_USER` | Usuario de PostgreSQL | Sí |
| `DB_PASSWORD` | Contraseña de PostgreSQL | Sí |
| `DB_NAME` | Nombre de la base de datos | Sí |
| `EMAIL_HOST` | Servidor SMTP | Sí |
| `EMAIL_PORT` | Puerto SMTP | Sí |
| `EMAIL_USER` | Usuario SMTP | Sí |
| `EMAIL_PASS` | Contraseña SMTP | Sí |
| `STRIPE_SECRET_KEY` | Clave secreta de Stripe | Para integración Stripe |
| `FACTURAS_API_URL` | URL API de facturas | Para integración facturas |
| `FACTURAS_ISSUER_RFC` | RFC del emisor | Para integración facturas |
| `FACTURAS_TYPE` | Tipo de factura (I/E) | Para integración facturas |
| `FACTURAS_API_TOKEN` | Token de API facturas | Para integración facturas |
| `BANXICO_TOKEN` | Token de Banxico | Para tipo de cambio |
---
## Patrones y Convenciones
1. **Un controller por dominio:** Todos los endpoints relacionados con un recurso viven en el mismo controller.
2. **Parámetros posicionales:** Las funciones SQL usan `$1, $2, $3...` en orden.
3. **Respuesta consistente:** Siempre retornar JSON con al menos `{ message, ...data }`.
4. **Errores 500:** Cualquier excepción se captura y responde con status 500.
5. **Sin ORM:** Las consultas son SQL crudo o llamadas a funciones almacenadas.
---
## Guía para Agregar un Nuevo Endpoint
1. **Crear/actualizar la función SQL** en PostgreSQL.
2. **Crear el método** en el controller correspondiente.
3. **Agregar la ruta** en el archivo `.routes.js` apropiado.
4. **Registrar la ruta** en `src/app.js` con un prefijo `/api/`.
5. **Probar** la función SQL directamente en PostgreSQL antes de probar el endpoint.
6. **Actualizar** `docs/API.md` con el nuevo endpoint.

355
docs/DATABASE.md Normal file
View File

@@ -0,0 +1,355 @@
# 🗄️ Documentación de Base de Datos — PostgreSQL
## Índice
1. [Filosofía de Diseño](#filosofía-de-diseño)
2. [Conexión](#conexión)
3. [Funciones SQL por Dominio](#funciones-sql-por-dominio)
4. [Tablas Principales (Inferidas)](#tablas-principales-inferidas)
5. [Patrones de Datos](#patrones-de-datos)
6. [JSONB como Patrón de Intercambio](#jsonb-como-patrón-de-intercambio)
7. [Integraciones que Escriben en JSONB](#integraciones-que-escriben-en-jsonb)
8. [Guía para Modificar Funciones SQL](#guía-para-modificar-funciones-sql)
---
## Filosofía de Diseño
Este sistema sigue un patrón **"Database-First"** donde la lógica de negocio reside principalmente en **funciones SQL almacenadas** de PostgreSQL. Los controllers de Node.js actúan como una capa de presentación HTTP liviana que solo orquesta llamadas a estas funciones.
**Implicaciones:**
- Para modificar lógica de negocio, debes modificar las funciones SQL, no solo el JavaScript.
- Los controllers pasan parámetros posicionales (`$1`, `$2`) y reciben resultados directamente.
- No hay ORM ni capa de abstracción de base de datos.
---
## Conexión
El backend usa `pg` (node-postgres) con un `Pool`:
```javascript
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
```
**Variables de entorno requeridas:** `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`.
---
## Funciones SQL por Dominio
### 🔐 Autenticación
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `validarusuario(name_mail_user, user_pass)` | `text`, `text` | `{status, rol, user_id, user_name}` | Valida credenciales |
| `createuser(name_user, id_rol, email, user_pass)` | `text`, `int`, `text`, `text` | `status` | Crea usuario |
| `reppassuser(user_mail, new_pass)` | `text`, `text` | `status` | Reemplaza contraseña |
### 👥 Empleados
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `getemployees()` | — | setof record | Lista paginada de empleados |
| `activeemployeesnumber()` | — | `int` | Total de empleados activos |
| `getoneemployee(rfcEmployee)` | `text` | setof record | Un empleado por RFC |
| `newemployee(...)` | 12 parámetros | `status` | Inserta empleado |
| `updateemployee(...)` | 12 parámetros | `status` | Actualiza empleado |
| `getattendance()` | — | setof record | Registros de asistencia |
### 📄 Contratos
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `getcontracts()` | — | setof record | Lista de contratos |
| `getinfocontract(id)` | `int` | setof record | Info de un contrato |
| `neartoend()` | — | setof record | Contratos próximos a vencer |
| `newcontract(...)` | varios | `status` | Crea contrato |
| `updatecontract(...)` | varios | `status` | Actualiza contrato |
| `positions()` | — | setof record | Catálogo de puestos |
| `areas()` | — | setof record | Catálogo de áreas |
| `bosses()` | — | setof record | Catálogo de jefes |
### 📦 Productos / Inventario
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `getproducts()` | — | setof record | Lista de productos |
| `getoneproduct(id)` | `int` | setof record | Un producto |
| `newproduct(..., jsonb, ...)` | 11 parámetros | `status` | Crea producto |
| `updateproduct(..., jsonb, ...)` | 12 parámetros | `status` | Actualiza producto |
| `discardproductsstock(id, quantity, reasons)` | `int`, `numeric`, `text` | `status` | Descarta stock |
| `getdiscardproduct()` | — | setof record | Productos descartados |
| `report_inventoryv2()` | — | setof record | Reporte de inventario |
| `stockadjusment()` | — | setof record | Ajustes pendientes |
| `setstockadjusmentsv2(jsonb)` | `jsonb` | `stockadjus` | Aplica ajustes |
| `consumptionstock(product_id, quantity, date, rfc)` | varios | `status` | Registra consumo |
| `getconsumptionreport()` | — | setof record | Reporte de consumos |
| `newsupplier(name, rfc, mail, phone)` | 4 parámetros | `status` | Crea proveedor |
| `updatesupplier(id, name, rfc, mail, phone)` | 5 parámetros | `status` | Actualiza proveedor |
| `disableSuppliers(id)` | `int` | `status` | Deshabilita proveedor |
| `newsuplier(...)` | varios | `status` | (posible duplicado ortográfico) |
### 💰 Gastos (Expenses)
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `newexpensev2(..., jsonb, ...)` | 14 parámetros | `status` | Crea gasto |
| `updateexpensev4(..., jsonb)` | 15 parámetros | `status` | Actualiza gasto |
| `getexpense(id)` | `int` | setof record | Obtiene un gasto |
| `getpendingappexpenses()` | — | setof record | Gastos pendientes de aprobación |
| `getapprovedappexpenses()` | — | setof record | Gastos aprobados |
| `getrejectedappexpenses()` | — | setof record | Gastos rechazados |
| `setapprovalstatus(id, status, approved_by)` | 3 parámetros | `status` | Actualiza estado de aprobación |
| `setpaymentstatusv2(id, status)` | 2 parámetros | `status` | Actualiza estado de pago |
| `pendingapppayments()` | — | setof record | Pagos pendientes |
| `countdelaypayments()` | — | `int` | Pagos con retraso |
| `totalspent()` | — | `numeric` | Total gastado |
| `totalapproved()` | — | `numeric` | Total aprobado |
| `totalrejected()` | — | `numeric` | Total rechazado |
| `report_expenses()` | — | setof record | Reporte de gastos |
| `paymentsreport()` | — | setof record | Reporte de pagos |
| `mainsupplier()` | — | setof record | Proveedor principal |
| `gettaxes()` | — | setof record | Catálogo de impuestos |
### 🛒 Compras / Purchase Entries
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `getpurchases()` | — | setof record | Detalles de compras pendientes |
| `purchaseentry(id, checking)` | `int`, `int` | `status` | Registra recepción física |
### 💳 Pagos Mensuales
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `newmonthlyexpensev2(...)` | 10 parámetros | `status` | Crea gasto mensual |
| `updatemonthlyexpensev2(...)` | 11 parámetros | `status` | Actualiza gasto mensual |
| `setpaymentstatusmonthlyv2(id, status)` | 2 parámetros | `status` | Actualiza estado de pago |
| `setpaymentstatusmonthlyv2(id, status, tax_id, total)` | 4 parámetros | `status` | Actualiza estado y monto |
| `refresh_monthly_expenses()` | — | `status` | Refresca gastos mensuales |
| `getmonthlypayments()` | — | setof record | Lista de pagos mensuales |
| `getONEmonthlypayment(id)` | `int` | setof record | Un pago mensual |
| `needToRefresh(id, boolean)` | 2 parámetros | rows | Controla actualización automática |
### 📧 Emails Programáticos
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `validateendpoint(endpoint_name)` | `text` | `boolean` | Verifica si se puede ejecutar (1x/mes) |
| `nearexpiring()` | — | setof record | Contratos por vencer |
| `paymentdelay()` | — | setof record | Pagos atrasados |
| `expensesneartodeadline()` | — | setof record | Gastos próximos a fecha límite |
| `expensesspecial()` | — | setof record | Gastos especiales |
| `birthdays()` | — | setof record | Cumpleaños |
| `contractexpired()` | — | setof record | Contratos vencidos |
| `expiredcontractsmonth()` | — | setof record | Contratos vencidos del mes |
### 📊 Ingresos (Little Hotelier)
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `getincomes(...)` | varios (filtros) | setof record | Lista de ingresos |
| `totalincomes(...)` | varios | `numeric` | Total de ingresos |
| `channelscards(...)` | varios | setof record | Canales de ingreso |
| `loadincomes(jsonb)` | `jsonb` | `status` | Carga masiva de ingresos |
| `loadproductsales(jsonb)` | `jsonb` | `status` | Carga masiva de ventas de productos |
| `loadchequesdetalle(jsonb)` | `jsonb` | `status` | Carga masiva de cheques |
| `reportincomes(...)` | varios | setof record | Reporte de ingresos |
| `countticket(...)` | varios | `int` | Conteo de tickets |
| `efectivo(...)`, `otros(...)`, `propinas(...)`, `tarjeta(...)`, `vales(...)` | varios | `numeric` | Desglose por tipo de pago |
| `sumatotal(...)` | varios | `numeric` | Suma total |
| `ticketpromedio(...)` | varios | `numeric` | Ticket promedio |
### 🏨 Horux / Facturas / Stripe
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `addhoruxdata(jsonb)` | `jsonb` | `int` (total insertados) | Inserta facturas masivamente |
| `getcategoryincome()` | — | setof record | Categorías de ingreso |
| `getinvoiceincome()` | — | setof record | Facturas de ingreso |
| `getaccountincome()` | — | setof record | Cuentas de ingreso |
| `getincomehorux()` | — | setof record | Ingresos Horux |
| `gettotalincome()` | — | `numeric` | Total de ingresos Horux |
| `getoneincome(id)` | `int` | setof record | Un ingreso Horux |
| `newincome(account_id, amount, date, invoice, area_id, categories::jsonb)` | 6 parámetros | `status` | Crea ingreso Horux |
| `updateincome(id, account_id, amount, date, invoice, area_id, categories::jsonb)` | 7 parámetros | `status` | Actualiza ingreso Horux |
| `addstripedatav2(jsonb)` | `jsonb` | `int` (inserted) | Inserta datos de Stripe |
### 📈 Hotel P&L / Restaurant P&L
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `cogs(...)` | varios (fechas) | setof record | Cost of Goods Sold |
| `ebitda(...)` | varios | setof record | EBITDA |
| `employeeshare(...)` | varios | setof record | Participación de empleados |
| `grossprofit(...)` | varios | setof record | Ganancia bruta |
| `tips(...)` | varios | setof record | Propinas |
| `totalrevenue(...)` | varios | setof record | Ingresos totales |
| `weightedCategoriesCost(...)` | varios | setof record | Costos ponderados por categoría |
### 💱 Tipo de Cambio
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `consultexchange(...)` | varios | setof record | Consulta tipo de cambio |
| `getexchanges()` | — | setof record | Historial de tipos de cambio |
### ⚙️ Configuraciones
| Función | Parámetros | Retorno | Descripción |
|---------|-----------|---------|-------------|
| `newroom(...)` | varios | `status` | Crea habitación |
| `newproperty(...)` | varios | `status` | Crea propiedad |
| `reportrooms()` | — | setof record | Reporte de habitaciones |
| `reportproperties()` | — | setof record | Reporte de propiedades |
| `approveby()` | — | setof record | Usuarios que pueden aprobar |
| `requestby()` | — | setof record | Usuarios que pueden solicitar |
| `categoryexpense()` | — | setof record | Categorías de gasto |
| `currency()` | — | setof record | Monedas |
| `units()` | — | setof record | Unidades de medida |
| `recurrence()` | — | setof record | Recurrencias (mensual, etc.) |
---
## Tablas Principales (Inferidas)
Basado en las funciones SQL y los controllers, las tablas principales son:
| Tabla | Propósito |
|-------|-----------|
| `employees` | Datos de empleados |
| `contracts` | Contratos laborales |
| `products` | Catálogo de productos |
| `suppliers` | Proveedores |
| `expenses` | Gastos/egresos |
| `purchase_details` | Líneas de compra asociadas a gastos |
| `inventory_entries` / `stock_movements` | Movimientos de inventario |
| `incomes` | Ingresos de Little Hotelier |
| `income_hrx` | Ingresos de Horux |
| `invoice_income` | Facturas descargadas |
| `stripe_data` | Datos de transfers de Stripe |
| `users` | Usuarios del sistema |
| `roles` | Roles de usuario |
| `areas` | Áreas del hotel |
| `positions` | Puestos de trabajo |
| `endpoint_logs` | Control de ejecución de emails (1x/mes) |
| `settings_rooms` | Habitaciones |
| `settings_properties` | Propiedades |
---
## Patrones de Datos
### Paginación Manual
```sql
-- En los controllers
SELECT * FROM getemployees() LIMIT $1 OFFSET $2;
SELECT COUNT(*) FROM employees;
```
### Estados Numéricos
Muchas funciones retornan un `status` numérico:
- `1` = Éxito
- `0` = Error/No permitido
- `-3` = Condición no cumplida (ej. "gasto no aprobado")
- Otros negativos = Errores específicos
---
## JSONB como Patrón de Intercambio
El sistema usa extensivamente `jsonb` para pasar datos complejos:
### Ejemplo: Productos en un Gasto
```javascript
// Controller
const products = [
{ id_product: 1, quantity: 10 },
{ id_product: 2, quantity: 5 }
];
const productJson = JSON.stringify(products);
await pool.query(
'SELECT newexpensev2($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11,$12,$13,$14)',
[..., productJson, ...]
);
```
### Ejemplo: Ajustes de Stock
```javascript
const stockproductadjusment = [
{ id_product: 1, new_stock: 50 },
{ id_product: 2, new_stock: 30 }
];
await pool.query(
'SELECT * FROM setstockadjusmentsv2($1::jsonb)',
[JSON.stringify(stockproductadjusment)]
);
```
### Ejemplo: Facturas Masivas
```javascript
const facturas = await getFacturas(desde, hasta);
await pool.query(
'SELECT addhoruxdata($1::jsonb)',
[JSON.stringify(facturas)]
);
```
---
## Integraciones que Escriben en JSONB
| Integración | Función SQL | Tabla Destino |
|-------------|-------------|---------------|
| Little Hotelier (CSV/XLSX) | `loadincomes(jsonb)` | `incomes` |
| Facturas API (México) | `addhoruxdata(jsonb)` | `income_hrx` / `invoice_income` |
| Stripe | `addstripedatav2(jsonb)` | `stripe_data` |
---
## Guía para Modificar Funciones SQL
### Paso 1: Verificar Dependencias
Busca TODOS los controllers que llaman a la función:
```bash
grep -r "nombrefuncion(" backend/hotel_hacienda/src/
```
### Paso 2: Probar en PostgreSQL Directamente
```sql
SELECT * FROM nombrefuncion('param1', 'param2');
-- o
SELECT nombrefuncion('param1') AS status;
```
### Paso 3: Considerar Idempotencia
Si la función modifica datos (INSERT/UPDATE), considera:
- ¿Qué pasa si se llama dos veces con los mismos parámetros?
- ¿Debería usar `ON CONFLICT` o verificar `IF EXISTS`?
### Paso 4: Actualizar Controllers si Cambia la Firma
Si agregas/quitas parámetros, actualiza TODOS los `pool.query(...)` que la llaman.
### Paso 5: Documentar el Cambio
Actualiza este archivo (`docs/DATABASE.md`) y `DOCUMENTACION_TECNICA.md`.

262
docs/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,262 @@
# 🚀 Guía de Despliegue
## Índice
1. [Requisitos del Servidor](#requisitos-del-servidor)
2. [Despliegue de Base de Datos](#despliegue-de-base-de-datos)
3. [Despliegue del Backend](#despliegue-del-backend)
4. [Despliegue del Frontend](#despliegue-del-frontend)
5. [Configuración de Proxy Inverso (Nginx)](#configuración-de-proxy-inverso-nginx)
6. [Configuración de PM2](#configuración-de-pm2)
7. [SSL con Certbot](#ssl-con-certbot)
8. [Verificación Post-Despliegue](#verificación-post-despliegue)
9. [Rollback](#rollback)
---
## Requisitos del Servidor
- Ubuntu 22.04 LTS (recomendado)
- 2GB RAM mínimo
- Node.js 18+
- PostgreSQL 13+
- Nginx
- PM2 (opcional pero recomendado)
- Git
---
## Despliegue de Base de Datos
### 1. Instalar PostgreSQL
```bash
sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl enable postgresql
```
### 2. Crear Base de Datos y Usuario
```bash
sudo -u postgres psql
```
```sql
CREATE DATABASE hotel_hacienda;
CREATE USER hoteluser WITH ENCRYPTED PASSWORD 'password_seguro';
GRANT ALL PRIVILEGES ON DATABASE hotel_hacienda TO hoteluser;
\q
```
### 3. Restaurar Backup
```bash
scp backupcondatos22122025.sql usuario@servidor:/tmp/
ssh usuario@servidor
sudo -u postgres psql -d hotel_hacienda -f /tmp/backupcondatos22122025.sql
```
### 4. Verificar Funciones
```bash
sudo -u postgres psql -d hotel_hacienda -c "\df"
```
---
## Despliegue del Backend
### 1. Clonar Repositorio
```bash
cd /var/www
git clone https://git.consultoria-as.com/consultoria-as/hotel-hacienda.git
cd hotel-hacienda/backend/hotel_hacienda
```
### 2. Instalar Dependencias
```bash
npm install --production
```
### 3. Configurar Variables de Entorno
```bash
cp .env.example .env
nano .env
```
```env
PORT=3000
URL_CORS=https://hotel.consultoria-as.com
DB_HOST=localhost
DB_PORT=5432
DB_USER=hoteluser
DB_PASSWORD=password_seguro
DB_NAME=hotel_hacienda
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=soporte@horuxfin.com
EMAIL_PASS=app_password
STRIPE_SECRET_KEY=sk_live_...
FACTURAS_API_URL=https://api.facturas.com/v1/
FACTURAS_ISSUER_RFC=RFC_DEL_HOTEL
FACTURAS_TYPE=I
FACTURAS_API_TOKEN=token_aqui
BANXICO_TOKEN=token_banxico
```
### 4. Iniciar con PM2
```bash
npm install -g pm2
pm2 start src/server.js --name hotel-api
pm2 save
pm2 startup
```
---
## Despliegue del Frontend
### 1. Compilar para Producción
```bash
cd /var/www/hotel-hacienda/frontend/Frontend-Hotel
npm install
```
### 2. Configurar Environment de Producción
```bash
echo "VITE_API_BASE_URL=https://hotel.consultoria-as.com/api" > .env
```
### 3. Build
```bash
npm run build
```
Esto genera la carpeta `dist/` con los archivos estáticos.
---
## Configuración de Proxy Inverso (Nginx)
### 1. Instalar Nginx
```bash
sudo apt install nginx
```
### 2. Configuración del Sitio
```bash
sudo nano /etc/nginx/sites-available/hotel-hacienda
```
```nginx
server {
listen 80;
server_name hotel.consultoria-as.com;
# Frontend estático
location / {
root /var/www/hotel-hacienda/frontend/Frontend-Hotel/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# Backend API
location /api/ {
proxy_pass http://localhost:3000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Logs
access_log /var/log/nginx/hotel-access.log;
error_log /var/log/nginx/hotel-error.log;
}
```
### 3. Activar Sitio
```bash
sudo ln -s /etc/nginx/sites-available/hotel-hacienda /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```
---
## SSL con Certbot
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d hotel.consultoria-as.com
sudo systemctl enable certbot.timer
```
---
## Verificación Post-Despliegue
### Checklist
- [ ] Backend responde: `curl https://hotel.consultoria-as.com/api/employees`
- [ ] Frontend carga sin errores en navegador
- [ ] Login funciona correctamente
- [ ] Base de datos tiene datos correctos
- [ ] Emails se envían (probar recuperación de contraseña)
- [ ] CORS permite el dominio correcto
- [ ] PM2 muestra el proceso activo: `pm2 list`
- [ ] Nginx no muestra errores: `sudo tail -f /var/log/nginx/hotel-error.log`
---
## Rollback
### Backend
```bash
pm2 stop hotel-api
# Restaurar código anterior con git
git checkout <commit-anterior>
npm install
pm2 restart hotel-api
```
### Base de Datos
```bash
# Restaurar backup previo
sudo -u postgres pg_dump hotel_hacienda > backup_antes_del_cambio.sql
sudo -u postgres psql -d hotel_hacienda -f backup_seguro.sql
```
### Frontend
```bash
# Recompilar versión anterior
git checkout <commit-anterior>
cd frontend/Frontend-Hotel
npm run build
```
---
> **Nota:** Mantener siempre un backup reciente de la base de datos antes de cualquier despliegue que modifique funciones SQL.

431
docs/FRONTEND.md Normal file
View File

@@ -0,0 +1,431 @@
# 🎨 Documentación del Frontend — Frontend-Hotel
## Índice
1. [Introducción](#introducción)
2. [Estructura de Carpetas](#estructura-de-carpetas)
3. [Entry Points](#entry-points)
4. [Enrutamiento](#enrutamiento)
5. [Manejo de Estado Global](#manejo-de-estado-global)
6. [Sistema de Permisos (Roles)](#sistema-de-permisos-roles)
7. [Comunicación con el Backend](#comunicación-con-el-backend)
8. [Componentes Reutilizables](#componentes-reutilizables)
9. [Estilos](#estilos)
10. [Páginas por Dominio](#páginas-por-dominio)
11. [Guía para Agregar una Nueva Página](#guía-para-agregar-una-nueva-página)
12. [Bugs y Limitaciones Conocidas](#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 `fetch` nativo y `axios`
- 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`
```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:**
1. `AuthProvider` — disponible en toda la app
2. `LangProvider` — depende de `AuthContext` (lee el rol para forzar español a housekeepers)
3. `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`).
```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: `:id` como 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`)
```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 `localStorage` bajo la clave `"rol"`.
- **NO exporta `useAuth`**, lo cual es un bug conocido.
- No usa JWT ni tokens.
### LangContext (`src/context/LenguageContext.jsx`)
```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)` o `useLang()`.
---
## 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`
```javascript
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
```env
VITE_API_BASE_URL=http://localhost:4000/api
```
Se accede en el frontend como:
```javascript
import.meta.env.VITE_API_BASE_URL
```
### Instancia Axios (`src/services/api.js`)
```javascript
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)
```javascript
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
```javascript
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 a `localhost:3000` o `localhost: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
```jsx
<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érica
- `DiscardConfirmModal.jsx` — Confirmación de descarte
- `ConfirmationMontlyPay.jsx` — Confirmación de pago mensual
- `ConfirmationOutcome.jsx` — Confirmación de salida
---
## Estilos
### Mezcla de Tecnologías
El proyecto usa tres sistemas de estilos simultáneamente:
1. **Tailwind CSS** — Configurado pero usado esporádicamente.
2. **Bootstrap 5** — Clases como `btn btn-primary`, `btn btn-secondary`.
3. **CSS Puro** — Un archivo `.css` por 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
1. **Crear el componente** en `src/pages/[Dominio]/NombrePagina.jsx`.
2. **Crear los estilos** opcionales en `src/pages/[Dominio]/NombrePagina.css`.
3. **Agregar la ruta** en `src/App.jsx` dentro de `<Route path="/app" element={<Layout />}>`.
4. **Agregar al menú** (si aplica):
- Editar `src/constants/menuconfig.js`
- Agregar la ruta a la sección correspondiente
5. **Actualizar permisos** en `src/components/Layout2.jsx`:
- Agregar lógica de `hidden` para la sección/submenú
- Agregar detección de ruta en `activeSection` si la ruta no sigue el patrón estándar
6. **Agregar traducciones** EN/ES si es texto visible.
7. **Consumir la API** usando `import.meta.env.VITE_API_BASE_URL`.
---
## Bugs y Limitaciones Conocidas
1. **`useAuth` no existe** — `AuthContext.jsx` no exporta `useAuth`, pero `ProtectedRoute.jsx` y `Navbar.jsx` lo intentan importar.
2. **No hay rutas protegidas**`App.jsx` no usa `ProtectedRoute`. Cualquiera puede acceder a `/app/*`.
3. **URLs hardcodeadas** — Varios `*Service.js` apuntan a `localhost` en vez de usar `VITE_API_BASE_URL`.
4. **Código comentado masivo** — Especialmente en `Layout.jsx` y páginas grandes.
5. **`multer` en frontend** — Es un middleware de Node.js, inapropiado para React.
6. **Sin tests** — No hay Jest, Vitest, ni Playwright configurados.

217
docs/TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,217 @@
# 🔧 Solución de Problemas Comunes
## Índice
1. [Backend](#backend)
2. [Frontend](#frontend)
3. [Base de Datos](#base-de-datos)
4. [Integraciones Externas](#integraciones-externas)
5. [Producción](#producción)
---
## Backend
### Error: "Conectado a PostgreSQL" no aparece
**Causa:** Variables de entorno de DB incorrectas o PostgreSQL no está corriendo.
**Solución:**
```bash
# Verificar PostgreSQL
sudo systemctl status postgresql
# Verificar variables de entorno
cat backend/hotel_hacienda/.env | grep DB_
# Probar conexión manual
node -e "require('./backend/hotel_hacienda/src/db/connection')"
```
### Error: CORS bloquea peticiones del frontend
**Causa:** `URL_CORS` no coincide con el dominio del frontend.
**Solución:**
```bash
# En backend/.env
URL_CORS=https://hotel.consultoria-as.com
# Sin slash al final
```
### Error: "Cannot find module 'pg'"
**Causa:** Dependencias no instaladas.
**Solución:**
```bash
cd backend/hotel_hacienda
npm install
```
### Emails no se envían
**Causa:** Credenciales SMTP incorrectas o servidor bloqueando.
**Solución:**
1. Verificar variables `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USER`, `EMAIL_PASS`
2. Para Gmail, usar "App Password", no la contraseña normal
3. Revisar logs: `pm2 logs hotel-api`
---
## Frontend
### Error: "Failed to fetch" o "Network Error"
**Causa:** `VITE_API_BASE_URL` apunta a localhost en producción.
**Solución:**
```bash
# Verificar .env del frontend
cat frontend/Frontend-Hotel/.env
# Debe ser:
VITE_API_BASE_URL=https://hotel.consultoria-as.com/api
```
### Página en blanco después del build
**Causa:** Rutas de React Router no configuradas en Nginx.
**Solución:** Asegurar que Nginx tenga:
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
### Error: `useAuth` is not exported
**Causa:** Bug conocido. `AuthContext.jsx` no exporta `useAuth`.
**Solución temporal:**
```jsx
// En vez de:
import { useAuth } from "../context/AuthContext";
// Usar:
import { AuthContext } from "../context/AuthContext";
const { user, login, logout } = useContext(AuthContext);
```
### Tailwind no aplica estilos
**Causa:** Tailwind v4 usa configuración diferente o `index.css` no tiene las directivas.
**Solución:** Verificar que `src/index.css` contenga:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
---
## Base de Datos
### Error: "function newexpensev2 does not exist"
**Causa:** Backup no restaurado completamente o función no migrada.
**Solución:**
```bash
# Restaurar backup completo
sudo -u postgres psql -d hotel_hacienda -f backupcondatos22122025.sql
# Verificar funciones existentes
sudo -u postgres psql -d hotel_hacienda -c "\df public.*"
```
### Error: "duplicate key value violates unique constraint"
**Causa:** Algunas funciones SQL no son idempotentes y se llaman dos veces.
**Solución:** Revisar la función SQL correspondiente. Agregar `IF EXISTS` o `ON CONFLICT`.
### Stock duplicado en inventario
**Causa:** Dos procesos tocan el mismo stock (ver análisis técnico en `DOCUMENTACION_TECNICA.md`, sección "Problema de duplicación en inventario").
**Solución:** Decidir si el stock se actualiza en `purchaseentry()` o en `setpaymentstatusv2()`, no en ambos.
---
## Integraciones Externas
### Stripe no devuelve datos
**Causa:** `STRIPE_SECRET_KEY` incorrecta o modo live/test mezclado.
**Solución:**
```bash
# Verificar que la key comience con sk_test_ o sk_live_
cat backend/.env | grep STRIPE
```
### API de Facturas devuelve error
**Causa:** Token expirado, RFC incorrecto, o fechas fuera de rango.
**Solución:**
1. Verificar `FACTURAS_API_TOKEN` y `FACTURAS_ISSUER_RFC`
2. Verificar que el rango de fechas no exceda el límite de la API
3. Revisar logs del backend
### Banxico retorna error
**Causa:** Token inválido o serie incorrecta.
**Solución:**
1. Verificar `BANXICO_TOKEN`
2. La serie usada es `SF43718` (Fix de USD/MXN)
3. Verificar que el token no haya expirado
---
## Producción
### PM2 no inicia al reiniciar servidor
**Solución:**
```bash
pm2 startup
pm2 save
```
### Nginx error 502 Bad Gateway
**Causa:** Backend no está corriendo o escucha en puerto diferente.
**Solución:**
```bash
# Verificar proceso
pm2 list
# Verificar puerto
sudo ss -tlnp | grep 3000
# Reiniciar
pm2 restart hotel-api
sudo systemctl restart nginx
```
### Certificado SSL expirado
**Solución:**
```bash
sudo certbot renew --dry-run
sudo certbot renew
sudo systemctl reload nginx
```
---
> Para problemas no listados aquí, revisar los logs detallados:
> - Backend: `pm2 logs hotel-api`
> - Nginx: `sudo tail -f /var/log/nginx/hotel-error.log`
> - PostgreSQL: `sudo tail -f /var/log/postgresql/postgresql-*.log`