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:
699
docs/API.md
Normal file
699
docs/API.md
Normal 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
365
docs/BACKEND.md
Normal 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
355
docs/DATABASE.md
Normal 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
262
docs/DEPLOYMENT.md
Normal 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
431
docs/FRONTEND.md
Normal 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
217
docs/TROUBLESHOOTING.md
Normal 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`
|
||||
Reference in New Issue
Block a user