Commit inicial - Sistema de Gestion Hotelera Hacienda San Angel
- Backend Node.js/Express con PostgreSQL - Frontend React 19 con Vite - Docker Compose para orquestacion - Documentacion completa en README.md - Scripts SQL para base de datos - Configuracion de ejemplo (.env.example)
This commit is contained in:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(git add:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Variables de entorno para Docker Compose
|
||||||
|
|
||||||
|
# Base de datos PostgreSQL
|
||||||
|
POSTGRES_PASSWORD=tu_password_seguro
|
||||||
|
|
||||||
|
# Configuracion de correo electronico (Brevo/Sendinblue)
|
||||||
|
EMAIL_USER=tu_email@ejemplo.com
|
||||||
|
EMAIL_PASS=tu_api_key
|
||||||
|
|
||||||
|
# Token de Banxico para tipo de cambio
|
||||||
|
BANXICO_TOKEN=tu_token_banxico
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Dependencias
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Variables de entorno (contienen secretos)
|
||||||
|
.env
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Archivos de build
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Sistema operativo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Archivos temporales
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Base de datos local
|
||||||
|
*.sql
|
||||||
|
!scripts bd/**/*.sql
|
||||||
|
|
||||||
|
# Archivos de respaldo
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
358
README.md
Normal file
358
README.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# Hacienda San Angel - Sistema de Gestion Hotelera
|
||||||
|
|
||||||
|
Sistema integral de gestion para el hotel Hacienda San Angel. Incluye modulos de administracion de empleados, nomina, inventario, gastos, ingresos, y reporteria.
|
||||||
|
|
||||||
|
## Tecnologias
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Node.js** con Express 5
|
||||||
|
- **PostgreSQL 15** como base de datos
|
||||||
|
- **Nodemailer** para envio de correos (Brevo/Sendinblue)
|
||||||
|
- **Stripe** para procesamiento de pagos
|
||||||
|
- **Axios** para consultas externas (tipo de cambio Banxico)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **React 19** con Vite 7
|
||||||
|
- **React Router DOM 7** para navegacion
|
||||||
|
- **React Hook Form** + **Yup** para validacion de formularios
|
||||||
|
- **Bootstrap 5** + **Tailwind CSS** para estilos
|
||||||
|
- **Axios** para peticiones HTTP
|
||||||
|
- **XLSX** para exportacion de datos a Excel
|
||||||
|
|
||||||
|
### Infraestructura
|
||||||
|
- **Docker** y **Docker Compose** para contenedorizacion
|
||||||
|
- **PostgreSQL 15** en contenedor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
Hotel/
|
||||||
|
├── backend/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── hotel_hacienda/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── app.js # Configuracion Express
|
||||||
|
│ │ ├── server.js # Punto de entrada
|
||||||
|
│ │ ├── controllers/ # Logica de negocio
|
||||||
|
│ │ ├── routes/ # Definicion de rutas API
|
||||||
|
│ │ ├── middlewares/ # Validadores
|
||||||
|
│ │ ├── db/ # Conexion a PostgreSQL
|
||||||
|
│ │ └── services/ # Servicios (email)
|
||||||
|
│ ├── scripts bd/ # Scripts SQL para la BD
|
||||||
|
│ └── package.json
|
||||||
|
├── frontend/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── Frontend-Hotel/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── App.jsx # Rutas principales
|
||||||
|
│ │ ├── main.jsx # Punto de entrada
|
||||||
|
│ │ ├── components/ # Componentes reutilizables
|
||||||
|
│ │ ├── pages/ # Paginas de la aplicacion
|
||||||
|
│ │ ├── services/ # Servicios API
|
||||||
|
│ │ ├── context/ # Context API
|
||||||
|
│ │ └── styles/ # Estilos globales
|
||||||
|
│ └── package.json
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .env.example
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modulos del Sistema
|
||||||
|
|
||||||
|
### 1. Dashboard
|
||||||
|
- **Income**: Vista general de ingresos
|
||||||
|
- **Hotel P&L**: Estado de perdidas y ganancias del hotel
|
||||||
|
- **Restaurant P&L**: Estado de perdidas y ganancias del restaurante
|
||||||
|
- **Budget**: Presupuesto
|
||||||
|
- **Cost Per Room**: Costo por habitacion
|
||||||
|
- **Room Analysis**: Analisis de ocupacion de habitaciones
|
||||||
|
- **Expenses**: Resumen de gastos
|
||||||
|
|
||||||
|
### 2. Nomina y Empleados (Payroll)
|
||||||
|
- Gestion de empleados (alta, baja, modificacion)
|
||||||
|
- Contratos laborales
|
||||||
|
- Control de asistencia
|
||||||
|
- Uniformes
|
||||||
|
- Pago diario
|
||||||
|
|
||||||
|
### 3. Gastos (Expenses)
|
||||||
|
- Registro de gastos
|
||||||
|
- Pagos mensuales recurrentes
|
||||||
|
- Aprobacion de gastos (flujo de aprobacion)
|
||||||
|
- Proveedores
|
||||||
|
- Reportes de gastos
|
||||||
|
- Entradas de compras
|
||||||
|
|
||||||
|
### 4. Inventario (Inventory)
|
||||||
|
- Catalogo de productos
|
||||||
|
- Ajustes de inventario
|
||||||
|
- Salidas de inventario
|
||||||
|
- Salidas de ama de llaves (Housekeeper)
|
||||||
|
- Reportes de inventario
|
||||||
|
- Descarte de productos
|
||||||
|
|
||||||
|
### 5. Ingresos (Income)
|
||||||
|
- Registro de ingresos
|
||||||
|
- Reportes de ingresos
|
||||||
|
- Integracion con sistema Horux
|
||||||
|
|
||||||
|
### 6. Hotel
|
||||||
|
- Gestion de propiedades
|
||||||
|
- Gestion de habitaciones
|
||||||
|
|
||||||
|
### 7. Configuracion (Settings)
|
||||||
|
- Gestion de habitaciones
|
||||||
|
- Configuracion del sistema
|
||||||
|
- Gestion de usuarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Autenticacion
|
||||||
|
- `POST /api/auth/login` - Iniciar sesion
|
||||||
|
- `POST /api/auth/create` - Crear usuario
|
||||||
|
- `POST /api/auth/recover` - Recuperar contrasena
|
||||||
|
|
||||||
|
### Empleados
|
||||||
|
- `GET /api/employees` - Listar empleados
|
||||||
|
- `POST /api/employees/new` - Crear empleado
|
||||||
|
- `PUT /api/employees/update` - Actualizar empleado
|
||||||
|
- `GET /api/employees/attendance` - Obtener asistencia
|
||||||
|
|
||||||
|
### Contratos
|
||||||
|
- `GET /api/contracts` - Listar contratos
|
||||||
|
- `POST /api/contracts` - Crear contrato
|
||||||
|
- `PUT /api/contracts` - Actualizar contrato
|
||||||
|
|
||||||
|
### Productos/Inventario
|
||||||
|
- `GET /api/products` - Listar productos
|
||||||
|
- `POST /api/products` - Crear producto
|
||||||
|
- `PUT /api/products` - Actualizar producto
|
||||||
|
|
||||||
|
### Gastos
|
||||||
|
- `GET /api/expenses` - Listar gastos
|
||||||
|
- `POST /api/expenses` - Crear gasto
|
||||||
|
- `PUT /api/expenses` - Actualizar gasto
|
||||||
|
- `GET /api/expenses/pending` - Gastos pendientes de aprobacion
|
||||||
|
|
||||||
|
### Ingresos
|
||||||
|
- `GET /api/incomes` - Listar ingresos
|
||||||
|
- `POST /api/incomes` - Registrar ingreso
|
||||||
|
- `GET /api/incomeshrx` - Ingresos Horux
|
||||||
|
|
||||||
|
### Pagos
|
||||||
|
- `POST /api/payment` - Procesar pago con Stripe
|
||||||
|
|
||||||
|
### Tipo de Cambio
|
||||||
|
- `GET /api/exchange` - Obtener tipo de cambio (Banxico)
|
||||||
|
|
||||||
|
### Configuracion
|
||||||
|
- `GET /api/settings` - Obtener configuracion
|
||||||
|
- `PUT /api/settings` - Actualizar configuracion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guia de Instalacion
|
||||||
|
|
||||||
|
### Requisitos Previos
|
||||||
|
- Docker y Docker Compose instalados
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Opcion 1: Instalacion con Docker (Recomendado)
|
||||||
|
|
||||||
|
1. **Clonar el repositorio**
|
||||||
|
```bash
|
||||||
|
git clone https://git.consultoria-as.com/usuario/Hacienda-San-Angel.git
|
||||||
|
cd Hacienda-San-Angel
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configurar variables de entorno**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Editar el archivo `.env` con los valores correspondientes:
|
||||||
|
```env
|
||||||
|
POSTGRES_PASSWORD=tu_password_seguro
|
||||||
|
EMAIL_USER=tu_email@ejemplo.com
|
||||||
|
EMAIL_PASS=tu_api_key_brevo
|
||||||
|
BANXICO_TOKEN=tu_token_banxico
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Construir e iniciar los contenedores**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verificar que los servicios esten corriendo**
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Importar la base de datos**
|
||||||
|
```bash
|
||||||
|
# Copiar el archivo SQL al contenedor
|
||||||
|
docker cp backupcondatos22122025.sql postgres_db:/tmp/
|
||||||
|
|
||||||
|
# Ejecutar el script SQL
|
||||||
|
docker exec -it postgres_db psql -U oposgres -d haciendasanangel -f /tmp/backupcondatos22122025.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Acceder a la aplicacion**
|
||||||
|
- Frontend: http://localhost:5172
|
||||||
|
- Backend API: http://localhost:4000/api
|
||||||
|
|
||||||
|
### Opcion 2: Instalacion Manual (Desarrollo)
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
1. **Navegar al directorio del backend**
|
||||||
|
```bash
|
||||||
|
cd backend/hotel_hacienda
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Instalar dependencias**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configurar variables de entorno**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar .env con los valores correspondientes
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Iniciar el servidor**
|
||||||
|
```bash
|
||||||
|
npm run dev # Desarrollo con nodemon
|
||||||
|
npm start # Produccion
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
|
||||||
|
1. **Navegar al directorio del frontend**
|
||||||
|
```bash
|
||||||
|
cd frontend/Frontend-Hotel
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Instalar dependencias**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configurar variables de entorno**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar .env con la URL del API
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Iniciar la aplicacion**
|
||||||
|
```bash
|
||||||
|
npm run dev # Desarrollo
|
||||||
|
npm run build # Construir para produccion
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base de Datos
|
||||||
|
|
||||||
|
### Configuracion
|
||||||
|
- **Motor**: PostgreSQL 15
|
||||||
|
- **Base de datos**: haciendasanangel
|
||||||
|
- **Usuario**: oposgres
|
||||||
|
|
||||||
|
### Scripts SQL
|
||||||
|
Los scripts de la base de datos se encuentran en `backend/hotel_hacienda/scripts bd/funcionesparaproduccion/`. Incluyen:
|
||||||
|
|
||||||
|
- Funciones para empleados y contratos
|
||||||
|
- Funciones para gastos e ingresos
|
||||||
|
- Funciones para inventario
|
||||||
|
- Funciones para reporteria
|
||||||
|
- Funciones de autenticacion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables de Entorno
|
||||||
|
|
||||||
|
### Backend (.env)
|
||||||
|
| Variable | Descripcion |
|
||||||
|
|----------|-------------|
|
||||||
|
| PORT | Puerto del servidor (default: 4000) |
|
||||||
|
| DB_HOST | Host de PostgreSQL |
|
||||||
|
| DB_PORT | Puerto de PostgreSQL (default: 5432) |
|
||||||
|
| DB_USER | Usuario de PostgreSQL |
|
||||||
|
| DB_PASSWORD | Contrasena de PostgreSQL |
|
||||||
|
| DB_NAME | Nombre de la base de datos |
|
||||||
|
| EMAIL_HOST | Servidor SMTP |
|
||||||
|
| EMAIL_PORT | Puerto SMTP |
|
||||||
|
| EMAIL_USER | Usuario SMTP |
|
||||||
|
| EMAIL_PASS | Contrasena/API Key SMTP |
|
||||||
|
| URL_CORS | URL permitida para CORS |
|
||||||
|
| BANXICO_TOKEN | Token API de Banxico |
|
||||||
|
| STRIPE_SECRET_KEY | Llave secreta de Stripe |
|
||||||
|
|
||||||
|
### Frontend (.env)
|
||||||
|
| Variable | Descripcion |
|
||||||
|
|----------|-------------|
|
||||||
|
| VITE_API_BASE_URL | URL base del API backend |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Despliegue en Produccion
|
||||||
|
|
||||||
|
### Con Docker Compose
|
||||||
|
|
||||||
|
1. Asegurarse de tener configurado el archivo `.env` con valores de produccion
|
||||||
|
|
||||||
|
2. Modificar `docker-compose.yml` si es necesario:
|
||||||
|
- Cambiar `URL_CORS` a tu dominio de produccion
|
||||||
|
- Configurar volumenes persistentes para la base de datos
|
||||||
|
|
||||||
|
3. Iniciar los servicios:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notas de Seguridad
|
||||||
|
- Cambiar las contrasenas por defecto
|
||||||
|
- Usar HTTPS en produccion (configurar con nginx o similar)
|
||||||
|
- Respaldar la base de datos regularmente
|
||||||
|
- No exponer el puerto de PostgreSQL al exterior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mantenimiento
|
||||||
|
|
||||||
|
### Respaldo de Base de Datos
|
||||||
|
```bash
|
||||||
|
docker exec postgres_db pg_dump -U oposgres haciendasanangel > backup_$(date +%Y%m%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f backend
|
||||||
|
docker-compose logs -f frontend
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reiniciar servicios
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Licencia
|
||||||
|
|
||||||
|
Proyecto privado - Hacienda San Angel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contacto
|
||||||
|
|
||||||
|
Para soporte tecnico, contactar al equipo de desarrollo.
|
||||||
19
backend/Dockerfile
Normal file
19
backend/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar archivos de dependencias
|
||||||
|
COPY hotel_hacienda/package*.json ./
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copiar codigo fuente
|
||||||
|
COPY hotel_hacienda/src ./src
|
||||||
|
COPY hotel_hacienda/index.js ./
|
||||||
|
|
||||||
|
# Exponer puerto
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
# Comando de inicio
|
||||||
|
CMD ["node", "src/server.js"]
|
||||||
24
backend/hotel_hacienda/.env.example
Normal file
24
backend/hotel_hacienda/.env.example
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Configuracion del servidor
|
||||||
|
PORT=4000
|
||||||
|
|
||||||
|
# Configuracion de base de datos PostgreSQL
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=oposgres
|
||||||
|
DB_PASSWORD=tu_password_seguro
|
||||||
|
DB_NAME=haciendasanangel
|
||||||
|
|
||||||
|
# Configuracion de correo electronico (Brevo/Sendinblue)
|
||||||
|
EMAIL_HOST=smtp-relay.brevo.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USER=tu_email@ejemplo.com
|
||||||
|
EMAIL_PASS=tu_api_key
|
||||||
|
|
||||||
|
# Configuracion de CORS
|
||||||
|
URL_CORS=http://localhost:5172
|
||||||
|
|
||||||
|
# Token de Banxico para tipo de cambio
|
||||||
|
BANXICO_TOKEN=tu_token_banxico
|
||||||
|
|
||||||
|
# Stripe (pagos)
|
||||||
|
STRIPE_SECRET_KEY=sk_test_xxx
|
||||||
139
backend/hotel_hacienda/.gitignore
vendored
Normal file
139
backend/hotel_hacienda/.gitignore
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Sveltekit cache directory
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Firebase cache directory
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Vite logs files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
2
backend/hotel_hacienda/README.md
Normal file
2
backend/hotel_hacienda/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# hotel_hacienda
|
||||||
|
Proyecto para el hotel_hacienda, trabajo independiente.
|
||||||
1
backend/hotel_hacienda/index.js
Normal file
1
backend/hotel_hacienda/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
require('./src/server.js');
|
||||||
1669
backend/hotel_hacienda/package-lock.json
generated
Normal file
1669
backend/hotel_hacienda/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
backend/hotel_hacienda/package.json
Normal file
29
backend/hotel_hacienda/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "api_hotelhacienda",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"csv-parser": "^3.2.0",
|
||||||
|
"dotenv": "^17.2.2",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"express-validator": "^7.2.1",
|
||||||
|
"nodemailer": "^7.0.10",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"stripe": "^20.1.2",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
51
backend/hotel_hacienda/src/app.js
Normal file
51
backend/hotel_hacienda/src/app.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const cors = require('cors');
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin: process.env.URL_CORS, // sin slash al final
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"]
|
||||||
|
};
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
//rutas
|
||||||
|
const authRoutes = require('./routes/auth.routes');
|
||||||
|
const employeeRoutes = require('./routes/employee.routes');
|
||||||
|
const contractsRoutes = require('./routes/contract.routes');
|
||||||
|
const reportcontractRoutes = require('./routes/reportcontract.routes');
|
||||||
|
const productRoutes = require('./routes/product.routes');
|
||||||
|
const expenseRoutes = require('./routes/expense.routes');
|
||||||
|
const statusRoutes = require('./routes/status.routes');
|
||||||
|
const paymentRoutes = require('./routes/payment.routes');
|
||||||
|
const settingsRoutes = require('./routes/settings.routes');
|
||||||
|
const emailRoutes = require('./routes/mail.routes');
|
||||||
|
const incomeRoutes = require('./routes/incomes.routes');
|
||||||
|
const purchaseRoutes = require('./routes/purchase.routes');
|
||||||
|
const exchangeRoutes = require('./routes/exchange.routes');
|
||||||
|
const hotelRoutes = require('./routes/hotelpl.routes');
|
||||||
|
const restaurantRoutes = require('./routes/restaurantpl.routes');
|
||||||
|
const incomehrxRoutes = require('./routes/incomehrx.routes');
|
||||||
|
|
||||||
|
//Prefijo
|
||||||
|
app.use('/api/employees', employeeRoutes);
|
||||||
|
app.use('/api/contracts', contractsRoutes);
|
||||||
|
app.use('/api/reportcontracts', reportcontractRoutes);
|
||||||
|
app.use('/api/products', productRoutes);
|
||||||
|
app.use('/api/auth', authRoutes);
|
||||||
|
app.use('/api/expenses', expenseRoutes);
|
||||||
|
app.use('/api/status', statusRoutes);
|
||||||
|
app.use('/api/payment', paymentRoutes);
|
||||||
|
app.use('/api/settings',settingsRoutes);
|
||||||
|
app.use('/api/emails',emailRoutes);
|
||||||
|
app.use('/api/incomes',incomeRoutes);
|
||||||
|
app.use('/api/incomeshrx',incomehrxRoutes);
|
||||||
|
app.use('/api/purchases',purchaseRoutes);
|
||||||
|
app.use('/api/exchange',exchangeRoutes);
|
||||||
|
app.use('/api/hotelpl',hotelRoutes);
|
||||||
|
app.use('/api/restaurantpl',restaurantRoutes);
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
102
backend/hotel_hacienda/src/controllers/auth.controller.js
Normal file
102
backend/hotel_hacienda/src/controllers/auth.controller.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const transporter = require('../services/mailService');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const login = async (req, res) => {
|
||||||
|
const { name_mail_user, user_pass } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * from validarusuario($1, $2)', [name_mail_user, user_pass]);
|
||||||
|
const { status, rol, user_id,user_name } = result.rows[0];
|
||||||
|
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
message = 'Usuario autenticado correctamente';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
message = 'Usuario o contraseña incorrectos';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = 'Error desconocido en la validación';
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
rol,
|
||||||
|
user_id,
|
||||||
|
user_name,
|
||||||
|
message });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'Error interno del servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createuser = async (req, res) => {
|
||||||
|
const { name_user,id_rol, email,user_pass } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT createuser($1, $2, $3, $4) AS status', [name_user, id_rol, email, user_pass]);
|
||||||
|
|
||||||
|
const status = result.rows[0].status;
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se agrego el usuario correctamente",
|
||||||
|
status});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo agregar al usuario' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const passRecover = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {user_mail } = req.body;
|
||||||
|
|
||||||
|
const new_pass = crypto
|
||||||
|
.randomBytes(12)
|
||||||
|
.toString('base64')
|
||||||
|
.slice(0, 12);
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT reppassuser($1,$2) AS status',
|
||||||
|
[user_mail, new_pass]
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = result.rows[0].status;
|
||||||
|
|
||||||
|
if (status !== 1) {
|
||||||
|
return res.json({
|
||||||
|
message: 'Correo NO registrado.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: user_mail,
|
||||||
|
subject: `Nueva contraseña`,
|
||||||
|
html: `
|
||||||
|
<h2>Recuperación de contraseña</h2>
|
||||||
|
<p>Se generó una nueva contraseña para el correo <b>${user_mail}</b></p>
|
||||||
|
<p><b>Contraseña:</b> ${new_pass}</p>
|
||||||
|
<p>Por favor mantenla y borra el correo una vez resguardada.</p>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con nueva contraseña.',
|
||||||
|
status
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'No se pudo mandar el correo de recuperación'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { login,createuser,passRecover};
|
||||||
298
backend/hotel_hacienda/src/controllers/contract.controller.js
Normal file
298
backend/hotel_hacienda/src/controllers/contract.controller.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const transporter = require('../services/mailService'); // importa el transporter
|
||||||
|
|
||||||
|
const getContracts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Leer query params con valores por defecto
|
||||||
|
const page = parseInt(req.query.page) || 1; // Página actual
|
||||||
|
const limit = parseInt(req.query.limit) || 1000; // Cantidad por página
|
||||||
|
const offset = (page - 1) * limit; // Desde dónde empezar
|
||||||
|
|
||||||
|
// Llamamos a la función con LIMIT y OFFSET
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM getcontracts() LIMIT $1 OFFSET $2`,
|
||||||
|
[limit, offset]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Obtener total para calcular páginas
|
||||||
|
const totalResult = await pool.query('SELECT COUNT(*) FROM contracts');
|
||||||
|
const total = parseInt(totalResult.rows[0].count);
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const contracts = result.rows.map(con => ({
|
||||||
|
id_contract: con.id_contract,
|
||||||
|
name_employee: con.name_employee,
|
||||||
|
position_employee: con.position_employee,
|
||||||
|
area_employee: con.area_employee,
|
||||||
|
contract_start: con.contratc_start,
|
||||||
|
contract_end: con.contratc_end,
|
||||||
|
uniforms: con.uniforms,
|
||||||
|
daily_pay: con.daily_pay,
|
||||||
|
status_contract: con.status_contract,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
data: contracts
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los contratos' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//CONTRATOS CERCA DE TERMINAR, 1 MES O MENOS DE TIEMPO
|
||||||
|
const neartoend = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM contratcsneartoend() as contractsneartoend',
|
||||||
|
);
|
||||||
|
const neartoend = result.rows[0].contractsneartoend;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Contratos cerca de terminar',
|
||||||
|
data: neartoend
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los contratos' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//listado de posiciones
|
||||||
|
const getpositions = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getpositions()',
|
||||||
|
);
|
||||||
|
const positions = result.rows.map(pos => ({
|
||||||
|
id_position: pos.id_position,
|
||||||
|
name_position: pos.name_position,
|
||||||
|
spanish_name: pos.spanish_name
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:positions
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las posiciones' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getareas = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getareas()',
|
||||||
|
);
|
||||||
|
const areas = result.rows.map(ar => ({
|
||||||
|
id_area: ar.id_area,
|
||||||
|
name_area: ar.name_area,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:areas
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las areas' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newContract = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
rfc_emp,
|
||||||
|
id_position_emp,
|
||||||
|
id_area_emp,
|
||||||
|
start_contract,
|
||||||
|
end_contract,
|
||||||
|
new_daily_pay,
|
||||||
|
boss_rfc,
|
||||||
|
boss_name,
|
||||||
|
uniforms} = req.body;
|
||||||
|
let message = "";
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT newcontract($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb) AS status',
|
||||||
|
[rfc_emp,id_position_emp,id_area_emp,start_contract,end_contract,new_daily_pay,boss_rfc,boss_name,JSON.stringify(uniforms)]);
|
||||||
|
const newcontract = result.rows[0].status;
|
||||||
|
if(newcontract == 1)
|
||||||
|
message = "Contrato generado correctamente";
|
||||||
|
else
|
||||||
|
message = "No se pudo agregar el contrato"
|
||||||
|
res.json({
|
||||||
|
message,
|
||||||
|
idNewContract: newcontract
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo generar el nuevo contrato' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateContract = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
rfc_emp,
|
||||||
|
id_position_emp,
|
||||||
|
id_area_emp,
|
||||||
|
start_contract,
|
||||||
|
end_contract,
|
||||||
|
status_contract,
|
||||||
|
new_daily_pay,
|
||||||
|
boss_rfc,
|
||||||
|
boss_name,
|
||||||
|
reason,
|
||||||
|
uniforms} = req.body;
|
||||||
|
const { id } = req.params;
|
||||||
|
let message = "";
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT updatecontract($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb) AS status',
|
||||||
|
[id,rfc_emp,id_position_emp,id_area_emp,start_contract,end_contract,status_contract,new_daily_pay,boss_rfc,boss_name,reason,JSON.stringify(uniforms)]);
|
||||||
|
const newcontract = result.rows[0].status;
|
||||||
|
if(newcontract == 1)
|
||||||
|
message = "Contrato actualizado correctamente";
|
||||||
|
else
|
||||||
|
message = "No se pudo actualizar el contrato"
|
||||||
|
res.json({
|
||||||
|
message,
|
||||||
|
idNewContract: newcontract
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo generar el nuevo contrato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContract = async (req, res) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM getcontracts() WHERE id_contract = $1 LIMIT 1 `,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const contracts = result.rows.map(con => ({
|
||||||
|
id_contract: con.id_contract,
|
||||||
|
name_employee: con.name_employee,
|
||||||
|
position_employee: con.position_employee,
|
||||||
|
area_employee: con.area_employee,
|
||||||
|
contract_start: con.contratc_start,
|
||||||
|
contract_end: con.contratc_end,
|
||||||
|
uniforms: con.uniforms,
|
||||||
|
daily_pay: con.daily_pay,
|
||||||
|
status_contract: con.status_contract
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
|
||||||
|
data: contracts
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los contratos' , error});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getbosses = async (req, res) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM getbosses()`
|
||||||
|
);
|
||||||
|
const bosses = result.rows.map(con => ({
|
||||||
|
rfc_boss: con.rfc_boss,
|
||||||
|
name_boss: con.name_boss,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
|
||||||
|
data: bosses
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudieron obtener los jefes' , error});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledcontracts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM disabledcontracts() as total',
|
||||||
|
);
|
||||||
|
const disabled = result.rows[0].contractsneartoend;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "contratos expirados",
|
||||||
|
data:disabled
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las areas' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const reportEmployeeContract = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM reportemployeecontract()',
|
||||||
|
);
|
||||||
|
const report = result.rows.map(rep => ({
|
||||||
|
name_employee: rep.name_employee,
|
||||||
|
rfc_employee: rep.rfc_employee,
|
||||||
|
nss_employee: rep.nss_employee,
|
||||||
|
name_position: rep.name_position,
|
||||||
|
spanish_name: rep.spanish_name,
|
||||||
|
name_area: rep.name_area,
|
||||||
|
addres_employee: rep.name_area,
|
||||||
|
phone_employee: rep.phone_employee,
|
||||||
|
email_employee: rep.email_employee ,
|
||||||
|
birthday: rep.birthday,
|
||||||
|
curp: rep.curp ,
|
||||||
|
name_study: rep.name_study,
|
||||||
|
id_contract: rep.id_contract ,
|
||||||
|
contratc_start:rep.contract_start,
|
||||||
|
contratc_end: rep.contract_end,
|
||||||
|
name_status: rep.name_status ,
|
||||||
|
daily_pay:rep.daily_pay ,
|
||||||
|
rfc_boss: rep.rfc_boss ,
|
||||||
|
name_boss: rep.name_boss ,
|
||||||
|
reasonfordismissal: rep.reasonfordismissal ,
|
||||||
|
name_contact: rep.name_contact,
|
||||||
|
tel_contact: rep.tel_contact ,
|
||||||
|
name_relationship: rep.name_relationship
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:report
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el reporte' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getContract,
|
||||||
|
getContracts,
|
||||||
|
neartoend,
|
||||||
|
getpositions,
|
||||||
|
getareas,
|
||||||
|
newContract,
|
||||||
|
updateContract,
|
||||||
|
disabledcontracts,
|
||||||
|
getbosses,
|
||||||
|
reportEmployeeContract
|
||||||
|
};
|
||||||
269
backend/hotel_hacienda/src/controllers/employee.controller.js
Normal file
269
backend/hotel_hacienda/src/controllers/employee.controller.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const getEmployees = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Leer query params con valores por defecto
|
||||||
|
const page = parseInt(req.query.page) || 1; // Página actual
|
||||||
|
const limit = parseInt(req.query.limit) || 500; // Cantidad por página
|
||||||
|
const offset = (page - 1) * limit; // Desde dónde empezar
|
||||||
|
|
||||||
|
// Llamamos a la función con LIMIT y OFFSET
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM getemployees() LIMIT $1 OFFSET $2`,
|
||||||
|
[limit, offset]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Obtener total para calcular páginas
|
||||||
|
const totalResult = await pool.query('SELECT COUNT(*) FROM employees');
|
||||||
|
const total = parseInt(totalResult.rows[0].count);
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const employees = result.rows.map(emp => ({
|
||||||
|
employee_rfc: emp.employee_rfc,
|
||||||
|
name_employee: emp.name_employee,
|
||||||
|
nss_employe: emp.nss_employe,
|
||||||
|
position_employee: emp.position_employee,
|
||||||
|
area_employee: emp.area_employee,
|
||||||
|
phone_employee: emp.phone_employee,
|
||||||
|
end_contract: emp.end_contract,
|
||||||
|
daily_pay: emp.daily_pay,
|
||||||
|
uniforms: emp.uniforms,
|
||||||
|
status: emp.status,
|
||||||
|
birthday: emp.birthday,
|
||||||
|
curp: emp.curp
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
data: employees
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener empleados' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//TOTAL DE EMPLEADOS CON CONTRATO ACTIVO
|
||||||
|
const getTotalActiveEmployees = async (req, res) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM activeemployeesnumber() as activenumber',
|
||||||
|
);
|
||||||
|
const activeEmployees = result.rows[0].activenumber;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Total de empleados activos',
|
||||||
|
data: activeEmployees
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el total de empleados ' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//OBTENER UN SOLO EMPLEADO
|
||||||
|
const getEmployee = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { rfcEmployee} = req.body;
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getoneemployee($1)',
|
||||||
|
[rfcEmployee]
|
||||||
|
);
|
||||||
|
|
||||||
|
const employee = result.rows.map(emp => ({
|
||||||
|
name_emp: emp.name_emp,
|
||||||
|
rfc_emp: emp.rfc_emp,
|
||||||
|
nss_emp: emp.nss_emp,
|
||||||
|
id_position_emp: emp.id_position_emp,
|
||||||
|
id_area_emp: emp.id_area_emp,
|
||||||
|
addres_emp:emp.addres_emp,
|
||||||
|
phone_emp: emp.phone_emp,
|
||||||
|
email_emp: emp.email_emp,
|
||||||
|
daily_pay: emp.daily_pay,
|
||||||
|
uniforms: emp.uniforms,
|
||||||
|
birthday: emp.birthday,
|
||||||
|
curp: emp.curp
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (result.rows.length > 0) {
|
||||||
|
res.json({
|
||||||
|
message: 'Empleado existio previamente',
|
||||||
|
data: employee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res.json({
|
||||||
|
message: 'El pleneado no existe.',
|
||||||
|
data: 0 // Devuelve la primera fila (o todas si quieres)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener empleado' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEmployee = async(req,res)=>{
|
||||||
|
try{
|
||||||
|
const {
|
||||||
|
name_emp,
|
||||||
|
rfc_emp,
|
||||||
|
nss_emp,
|
||||||
|
addres_emp,
|
||||||
|
phone_emp,
|
||||||
|
email_emp,
|
||||||
|
birthday_emp,
|
||||||
|
curp_emp,
|
||||||
|
study_emp ,
|
||||||
|
emergency_name ,
|
||||||
|
emergency_tel,
|
||||||
|
relationship_id
|
||||||
|
} = req.body;
|
||||||
|
const result = await pool.query('SELECT newemployee($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) AS STATUS',
|
||||||
|
[name_emp,rfc_emp,nss_emp,addres_emp,phone_emp,email_emp,birthday_emp,curp_emp, study_emp, emergency_name, emergency_tel,relationship_id]
|
||||||
|
);
|
||||||
|
const status = result.rows[0].status;
|
||||||
|
if(status == 1)
|
||||||
|
message = "Empleado añadido correctamente";
|
||||||
|
else
|
||||||
|
message = "Empleado no se pudo agregar"
|
||||||
|
res.json({
|
||||||
|
message,
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const updateEmployee = async(req,res)=>{
|
||||||
|
try{
|
||||||
|
const {
|
||||||
|
name_emp,
|
||||||
|
rfc_emp,
|
||||||
|
nss_emp,
|
||||||
|
addres_emp,
|
||||||
|
phone_emp,
|
||||||
|
email_emp,
|
||||||
|
birthday_emp,
|
||||||
|
curp_emp,
|
||||||
|
study_emp ,
|
||||||
|
emergency_name ,
|
||||||
|
emergency_tel,
|
||||||
|
relationship_id
|
||||||
|
} = req.body;
|
||||||
|
const result = await pool.query('SELECT updateemployee($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) AS STATUS',
|
||||||
|
[name_emp,rfc_emp,nss_emp,addres_emp,phone_emp,email_emp,birthday_emp,curp_emp, study_emp,
|
||||||
|
emergency_name, emergency_tel, relationship_id]
|
||||||
|
);
|
||||||
|
const status = result.rows[0].status;
|
||||||
|
if(status == 1)
|
||||||
|
message = "Empleado actualizado correctamente";
|
||||||
|
else
|
||||||
|
message = "No se pudo agregar el empleado"
|
||||||
|
res.json({
|
||||||
|
message,
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const getattendance = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getattendance()',
|
||||||
|
);
|
||||||
|
|
||||||
|
const attendance = result.rows.map(emp => ({
|
||||||
|
aid_attendance: emp.aid_attendance,
|
||||||
|
aname_emp: emp.aname_emp,
|
||||||
|
alast_name: emp.alast_name,
|
||||||
|
aID_tran: emp.aID_tran,
|
||||||
|
arol: emp.arol,
|
||||||
|
adate_atten:emp.adate_atten,
|
||||||
|
atime_atten: emp.atime_atten,
|
||||||
|
alaboral_day: emp.alaboral_day,
|
||||||
|
adata_origin: emp.adata_origin,
|
||||||
|
aname_device: emp.aname_device,
|
||||||
|
aserie_device: emp.aserie_device,
|
||||||
|
aperforation: emp.aperforation,
|
||||||
|
aubication: emp.aubication,
|
||||||
|
aobservations: emp.aobservations
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'attendance',
|
||||||
|
data: attendance
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener checador' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradeOfStudy = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM degreeofstudy',
|
||||||
|
);
|
||||||
|
|
||||||
|
const study = result.rows.map(emp => ({
|
||||||
|
id_study: emp.id_study,
|
||||||
|
name_study: emp.name_study
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'grados de estudio',
|
||||||
|
data: study
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los grados de estudio' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getRelationshipEmployee = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM relationship_employee',
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationship = result.rows.map(emp => ({
|
||||||
|
id_relationship: emp.id_relationship,
|
||||||
|
name_relationship: emp.name_relationship
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Parentesco',
|
||||||
|
data: relationship
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los parentescos' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
{ getEmployees,
|
||||||
|
getTotalActiveEmployees,
|
||||||
|
getEmployee,
|
||||||
|
newEmployee,
|
||||||
|
updateEmployee,
|
||||||
|
getattendance,
|
||||||
|
getGradeOfStudy,
|
||||||
|
getRelationshipEmployee
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const axios = require('axios');
|
||||||
|
const BANXICO_SERIE = 'SF43718';
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapBanxicoData(bmxJson) {
|
||||||
|
const serie = bmxJson.bmx.series[0];
|
||||||
|
return serie.datos.map(item => {
|
||||||
|
const [day, month, year] = item.fecha.split('/');
|
||||||
|
return {
|
||||||
|
fecha: `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`,
|
||||||
|
valor: parseFloat(item.dato)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const consultExchange = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const token = process.env.BANXICO_TOKEN;
|
||||||
|
if (!token) return res.status(400).json({ error: 'No se encontró token de Banxico' });
|
||||||
|
|
||||||
|
const today = formatDate(new Date());
|
||||||
|
const url = `https://www.banxico.org.mx/SieAPIRest/service/v1/series/${BANXICO_SERIE}/datos/2021-11-07/${today}`;
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: { 'Bmx-Token': token, 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const datos = mapBanxicoData(response.data);
|
||||||
|
|
||||||
|
if (datos.length === 0) return res.status(404).json({ message: 'No hay datos para la fecha actual.' });
|
||||||
|
|
||||||
|
let savedCount = 0;
|
||||||
|
for (const item of datos) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO exchangetype(fecha, valor)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (fecha) DO NOTHING`,
|
||||||
|
[item.fecha, item.valor]
|
||||||
|
);
|
||||||
|
if (result.rowCount > 0) savedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = savedCount > 0
|
||||||
|
? 'Datos de tipo de cambio guardados correctamente'
|
||||||
|
: 'No se pudieron guardar los datos (quizá ya existían)';
|
||||||
|
|
||||||
|
res.json({ message, savedCount, datos });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error consultExchange:', err.response ? err.response.data : err);
|
||||||
|
res.status(500).json({ error: 'Error interno al consultar exchange' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExchange = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM getexange()');
|
||||||
|
const units = result.rows.map(u => ({
|
||||||
|
id: u.id_exchange,
|
||||||
|
fecha: u.fecha,
|
||||||
|
dato: parseFloat(u.dato)
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los tipos de cambio.",
|
||||||
|
request: units
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo recuperar los tipos de cambio.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { consultExchange, getExchange };
|
||||||
699
backend/hotel_hacienda/src/controllers/expense.controller.js
Normal file
699
backend/hotel_hacienda/src/controllers/expense.controller.js
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const transporter = require('../services/mailService');
|
||||||
|
|
||||||
|
const getPendingAppExpenses = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getpendingappexpenses()');
|
||||||
|
|
||||||
|
const status_approval = result.rows.map(pxp => ({
|
||||||
|
id_expense: pxp.id_expense,
|
||||||
|
expense_description: pxp.description,
|
||||||
|
request_date: pxp.request_date,
|
||||||
|
requested_by: pxp.name_user,
|
||||||
|
area: pxp.name_area,
|
||||||
|
amount: pxp.total,
|
||||||
|
status_appr: pxp.status_approval
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:status_approval
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener gastos pendientes.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getApprovedAppExpenses = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getapprovedappexpenses()');
|
||||||
|
|
||||||
|
const status_approval = result.rows.map(pxp => ({
|
||||||
|
id_expense: pxp.id_expense,
|
||||||
|
expense_description: pxp.description,
|
||||||
|
request_date: pxp.request_date,
|
||||||
|
approval_date: pxp.approval_date,
|
||||||
|
requested_by: pxp.name_user,
|
||||||
|
area: pxp.name_area,
|
||||||
|
amount: pxp.total,
|
||||||
|
status_appr: pxp.status_approval
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:status_approval
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener gastos aprobados.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const getRejectedappExpenses = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getrejectedappexpenses()');
|
||||||
|
|
||||||
|
const status_approval = result.rows.map(pxp => ({
|
||||||
|
id_expense: pxp.id_expense,
|
||||||
|
expense_description: pxp.description,
|
||||||
|
request_date: pxp.request_date,
|
||||||
|
reject_date: pxp.reject_date,
|
||||||
|
requested_by: pxp.name_user,
|
||||||
|
area: pxp.name_area,
|
||||||
|
amount: pxp.total,
|
||||||
|
status_appr: pxp.status_approval
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:status_approval
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener gastos rechazados.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalApproved = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {option} = req.body;
|
||||||
|
if(option ==1 )
|
||||||
|
{
|
||||||
|
const result = await pool.query('SELECT totalApproved() as total');
|
||||||
|
const total = result.rows[0].total;
|
||||||
|
res.json({
|
||||||
|
message: "Total aprobado",
|
||||||
|
data:total
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (option ==2){
|
||||||
|
const result = await pool.query('SELECT totalrejected() as total');
|
||||||
|
const total = result.rows[0].total;
|
||||||
|
res.json({
|
||||||
|
message: "Total rechazado",
|
||||||
|
data:total });
|
||||||
|
}else {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: "El parámetro 'option' solo puede ser 1 (aprobado) o 2 (rechazado).",
|
||||||
|
data: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el total de aprobados.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mainSupplier = async (req, res) => {
|
||||||
|
try{
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT * from mainsupplier()');
|
||||||
|
const mainSupp = result.rows.map(ms => ({
|
||||||
|
supplier: ms.name_suppliers,
|
||||||
|
percentage: ms.percentage,
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Proveedor Principal",
|
||||||
|
data:mainSupp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el total de aprobados.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReportExpenses = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM report_expenses()');
|
||||||
|
|
||||||
|
const reportExpenses = result.rows.map(re => ({
|
||||||
|
id_expense: re.id_expense,
|
||||||
|
expense_description: re.description,
|
||||||
|
name_suppliers: re.name_suppliers,
|
||||||
|
rfc_suppliers: re.rfc_suppliers,
|
||||||
|
request_date: re.request_date,
|
||||||
|
approval_date: re.approval_date,
|
||||||
|
payment_date: re.payment_date,
|
||||||
|
requested_by: re.requested_by,
|
||||||
|
totalpesos: re.totalpesos,
|
||||||
|
totaldolares: re.totaldolares,
|
||||||
|
category: re.category,
|
||||||
|
area: re.area,
|
||||||
|
status_approval: re.approval_status,
|
||||||
|
status_payment: re.payment_status,
|
||||||
|
is_monthly : re.is_monthly
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:reportExpenses
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el reporte.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReportPayments = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM paymentsreport()');
|
||||||
|
|
||||||
|
const reportPayment = result.rows.map(rp => ({
|
||||||
|
id_expense: rp.id_expense,
|
||||||
|
expense_description: rp.description,
|
||||||
|
approval_date: rp.approval_date,
|
||||||
|
area: rp.area,
|
||||||
|
supplier: rp.supplier,
|
||||||
|
totalpesos: rp.totalpesos,
|
||||||
|
totaldolares: rp.totaldolares,
|
||||||
|
status_approval: rp.approval_status,
|
||||||
|
status_payment: rp.payment_status
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:reportPayment
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el reporte de pagos.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const countpending = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {option} = req.body;
|
||||||
|
if(option ==1 ) //OBtienes cuantas aprobaciones tienes pendientes
|
||||||
|
{
|
||||||
|
const result = await pool.query('SELECT countpendingapprove() as countpen');
|
||||||
|
const total = result.rows[0].countpen;
|
||||||
|
res.json({
|
||||||
|
message: "Pagos pendientes de aprobación",
|
||||||
|
data:total
|
||||||
|
});
|
||||||
|
}else if(option ==2) //Obtienes los pagos pendientes de los gastos mensuales
|
||||||
|
{
|
||||||
|
const result = await pool.query('SELECT countpendingmonthly() as monthpen');
|
||||||
|
const total = result.rows[0].monthpen;
|
||||||
|
res.json({
|
||||||
|
message: "Pagos mensuales pendientes",
|
||||||
|
data:total
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el conteo de pendientes.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getmonthlypayments = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getmonthlypayments()');
|
||||||
|
|
||||||
|
const monthlypayment = result.rows.map(mp => ({
|
||||||
|
id_expense: mp.id_expense,
|
||||||
|
expense_description: mp.description,
|
||||||
|
recurrence_name: mp.recurrence_name,
|
||||||
|
payment_type: mp.payment_type,
|
||||||
|
payment_deadline: mp.payment_deadline,
|
||||||
|
area: mp.name_area,
|
||||||
|
supplier: mp.name_suppliers,
|
||||||
|
status: mp.status,
|
||||||
|
total: mp.total,
|
||||||
|
status_payment: mp.name_pay_status
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:monthlypayment
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el reporte de pagos mensuales.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const getExpense = async(req,res) =>
|
||||||
|
{
|
||||||
|
const { id } = req.params;
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getexpense($1)',[parseInt(id)]);
|
||||||
|
|
||||||
|
const expense = result.rows.map(exp => ({
|
||||||
|
id_exp: exp.id_exp,
|
||||||
|
description_exp: exp.description_exp,
|
||||||
|
suppliers_id: exp.suppliers_id,
|
||||||
|
request_date: exp.request_date,
|
||||||
|
payment_deadline: exp.payment_deadline,
|
||||||
|
request_by: exp.request_by,
|
||||||
|
approval_by: exp.approval_by,
|
||||||
|
id_area: exp.id_area,
|
||||||
|
category_exp: exp.category_exp,
|
||||||
|
currency: exp.currency,
|
||||||
|
subtotal: exp.subtotal,
|
||||||
|
iva: exp.iva,
|
||||||
|
ieps: exp.ieps,
|
||||||
|
total: exp.total,
|
||||||
|
needtoapprove: exp.needtoapprove,
|
||||||
|
products: exp.products,
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:expense
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInfoExpense = async(req,res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const infoSuppliers = await pool.query('SELECT * FROM getsuppliers()');
|
||||||
|
const infoCategories = await pool.query('SELECT * FROM getcategoryexpense()');
|
||||||
|
const infoAreas = await pool.query('SELECT * FROM getareas()');
|
||||||
|
const infoUsers = await pool.query('SELECT * FROM getapproveby()');
|
||||||
|
const infoCurrencies = await pool.query('SELECT * FROM getcurrency()');
|
||||||
|
const infoUniforms = await pool.query('SELECT * FROM getuniforms()');
|
||||||
|
const taxes = await pool.query('SELECT * FROM gettaxes()');
|
||||||
|
|
||||||
|
const suppliers = infoSuppliers.rows.map(supp => ({
|
||||||
|
id: supp.id_supplier,
|
||||||
|
rfc: supp.rfc_suppliers,
|
||||||
|
name: supp.name_suppliers
|
||||||
|
}));
|
||||||
|
const categories = infoCategories.rows.map(cat => ({
|
||||||
|
id: cat.id_category,
|
||||||
|
name: cat.name_category,
|
||||||
|
spanish_name: cat.spanish_name
|
||||||
|
}));
|
||||||
|
const areas = infoAreas.rows.map(area => ({
|
||||||
|
id: area.id_area,
|
||||||
|
name: area.name_area,
|
||||||
|
}));
|
||||||
|
const users = infoUsers.rows.map(user => ({
|
||||||
|
id: user.id_user,
|
||||||
|
name: user.user_name,
|
||||||
|
}));
|
||||||
|
const currencies = infoCurrencies.rows.map(cur => ({
|
||||||
|
id: cur.id_currency,
|
||||||
|
name: cur.name_currency,
|
||||||
|
}));
|
||||||
|
const uniforms = infoUniforms.rows.map(uni => ({
|
||||||
|
id: uni.id_uniform,
|
||||||
|
name: uni.name_uniform
|
||||||
|
}));
|
||||||
|
const tax = taxes.rows.map(uni => ({
|
||||||
|
id: uni.id_tax,
|
||||||
|
name: uni.name_tax,
|
||||||
|
number: uni.number
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
suppliers,
|
||||||
|
categories,
|
||||||
|
areas,
|
||||||
|
users,
|
||||||
|
currencies,
|
||||||
|
uniforms,
|
||||||
|
tax
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener la información especifica.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newExpense = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {new_description,
|
||||||
|
suppliers_id,
|
||||||
|
new_request_date,
|
||||||
|
new_payment_deadline,
|
||||||
|
request_by,
|
||||||
|
area,
|
||||||
|
expense_cat,
|
||||||
|
currency_id,
|
||||||
|
products,
|
||||||
|
new_iva,
|
||||||
|
new_ieps,
|
||||||
|
new_subtotal,
|
||||||
|
new_total,
|
||||||
|
needtoapprove
|
||||||
|
}= req.body;
|
||||||
|
|
||||||
|
const product = Array.isArray(products) && products.length > 0 ? JSON.stringify(products) : null;
|
||||||
|
const result = await pool.query('SELECT newexpensev2($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11,$12,$13,$14) AS STATUS',[
|
||||||
|
new_description,suppliers_id,new_request_date,new_payment_deadline,request_by,
|
||||||
|
area,expense_cat,currency_id,needtoapprove,product,new_subtotal,new_iva,new_ieps,new_total]);
|
||||||
|
const userEmail = await pool.query('SELECT * FROM getusersmailowner()');
|
||||||
|
|
||||||
|
const newExpense = result.rows[0].status;
|
||||||
|
const user = userEmail.rows;
|
||||||
|
/*let fechaFormateada = 'No definida';
|
||||||
|
if (new_payment_deadline) {
|
||||||
|
try {
|
||||||
|
const fecha = new Date(new_payment_deadline);
|
||||||
|
fechaFormateada = isNaN(fecha)
|
||||||
|
? (() => {
|
||||||
|
const [dia, mes, anio] = new_payment_deadline.split('/');
|
||||||
|
return new Date(`${anio}-${mes}-${dia}`).toLocaleDateString('en-US');
|
||||||
|
})()
|
||||||
|
: fecha.toLocaleDateString('en-US');
|
||||||
|
} catch {
|
||||||
|
fechaFormateada = 'Fecha inválida';
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
const productosHTML = Array.isArray(products)
|
||||||
|
? products.map(p => `<li>${p.id_product || 'N/A'} - QUANTITY: ${p.quantity || 1}</li>`).join('')
|
||||||
|
: 'without products associate';
|
||||||
|
const recipients = user.map(u => u.email);
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: `Approval of the new expense ${newExpense}`,
|
||||||
|
html: `<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
Expenditure approval required
|
||||||
|
</h2>
|
||||||
|
<p style="margin:6px 0 0 0;color:#f3dcdc;font-size:13px;">
|
||||||
|
Action pending on your part
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
An expense has been recorded that requires your approval.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="background-color:#faf5f6;border-left:4px solid #7a0c1e;
|
||||||
|
padding:12px 14px;margin:16px 0;">
|
||||||
|
<strong style="color:#7a0c1e;">ID EXPENSE:</strong>
|
||||||
|
<span style="font-size:15px;">${newExpense}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;"><strong>Description:</strong></td>
|
||||||
|
<td style="padding:6px 0;">${new_description}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;"><strong>Deadline:</strong></td>
|
||||||
|
<td style="padding:6px 0;">${new_payment_deadline}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;"><strong>Total:</strong></td>
|
||||||
|
<td style="padding:6px 0;font-weight:bold;color:#7a0c1e;">
|
||||||
|
${new_total || 'N/A'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="margin-top:18px;">
|
||||||
|
<p style="margin-bottom:6px;"><strong>Products included:</strong></p>
|
||||||
|
<ul style="padding-left:18px;margin:0;">
|
||||||
|
${productosHTML}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="text-align:center;margin-top:32px;">
|
||||||
|
<a href="https://hacienda.consultoria-as.com/"
|
||||||
|
style="background-color:#7a0c1e;color:#ffffff;
|
||||||
|
text-decoration:none;font-weight:bold;
|
||||||
|
padding:14px 28px;border-radius:6px;
|
||||||
|
display:inline-block;">
|
||||||
|
Review and approve spending
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
We recommend reviewing this application before the deadline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Financial management system · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>`,
|
||||||
|
};
|
||||||
|
if(needtoapprove== true){
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
};
|
||||||
|
res.json({
|
||||||
|
message: "Gasto agregado correctamente.",
|
||||||
|
new_expense: newExpense
|
||||||
|
});
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateExpense = async (req, res) => {
|
||||||
|
try{
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
const { id } = req.params;
|
||||||
|
const {up_description,
|
||||||
|
suppliers_id,
|
||||||
|
up_req_date,
|
||||||
|
up_deadline,
|
||||||
|
up_currency,
|
||||||
|
up_request_by,
|
||||||
|
up_area,
|
||||||
|
up_category,
|
||||||
|
needtoapprove,
|
||||||
|
up_subtotal,
|
||||||
|
up_iva,
|
||||||
|
up_ieps,
|
||||||
|
up_total,
|
||||||
|
products
|
||||||
|
}= req.body;
|
||||||
|
|
||||||
|
const product = Array.isArray(products) && products.length > 0 ? JSON.stringify(products) : null;
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT updateexpensev4($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15::jsonb) AS status',
|
||||||
|
[parseInt(id),up_description,suppliers_id,up_req_date,up_deadline,up_currency,
|
||||||
|
up_request_by,up_area,up_category,needtoapprove,up_subtotal,up_iva,up_ieps,up_total,product]);
|
||||||
|
const userEmail = await pool.query('SELECT * FROM getusersmailowner()');
|
||||||
|
|
||||||
|
const user = userEmail.rows;
|
||||||
|
const upExpense = result.rows[0].status;
|
||||||
|
switch (upExpense) {
|
||||||
|
case 0:
|
||||||
|
message = 'El gasto ya fue pagado, no se puede actualizar';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
message = 'El gasto se actualizo correctamente';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = 'Error desconocido en la validación';
|
||||||
|
};
|
||||||
|
|
||||||
|
/*let fechaFormateada = 'No definida';
|
||||||
|
if (up_deadline) {
|
||||||
|
try {
|
||||||
|
// Manejo de formato "DD-MM-YYYY"
|
||||||
|
if (typeof up_deadline === 'string' && up_deadline.includes('-')) {
|
||||||
|
const [dia, mes, anio] = up_deadline.split('-');
|
||||||
|
const fecha = new Date(`${anio}-${mes}-${dia}`);
|
||||||
|
fechaFormateada = !isNaN(fecha)
|
||||||
|
? fecha.toLocaleDateString('es-MX', { timeZone: 'UTC' })
|
||||||
|
: 'Fecha inválida';
|
||||||
|
} else {
|
||||||
|
const fecha = new Date(up_deadline);
|
||||||
|
fechaFormateada = !isNaN(fecha)
|
||||||
|
? fecha.toLocaleDateString('es-MX', { timeZone: 'UTC' })
|
||||||
|
: 'Fecha inválida';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error al formatear fecha:', err);
|
||||||
|
fechaFormateada = 'Fecha inválida';
|
||||||
|
}
|
||||||
|
};*/
|
||||||
|
const productosHTML = Array.isArray(products)
|
||||||
|
? products.map(p => `<li>${p.id_product || 'N/A'} - Quantity: ${p.quantity || 1}</li>`).join('')
|
||||||
|
: 'Without products associate';
|
||||||
|
const recipients = user.map(u => u.email);
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: `An updated ID expense approval is required: ${id}`,
|
||||||
|
html: `<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
Expenditure approval required
|
||||||
|
</h2>
|
||||||
|
<p style="margin:6px 0 0 0;color:#f3dcdc;font-size:13px;">
|
||||||
|
Action pending on your part
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
An expense has been updated that requires your approval.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="background-color:#faf5f6;border-left:4px solid #7a0c1e;
|
||||||
|
padding:12px 14px;margin:16px 0;">
|
||||||
|
<strong style="color:#7a0c1e;">ID EXPENSE:</strong>
|
||||||
|
<span style="font-size:15px;">${id}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;"><strong>Description:</strong></td>
|
||||||
|
<td style="padding:6px 0;">${up_description}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;"><strong>Deadline:</strong></td>
|
||||||
|
<td style="padding:6px 0;">${up_deadline}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;"><strong>Total:</strong></td>
|
||||||
|
<td style="padding:6px 0;font-weight:bold;color:#7a0c1e;">
|
||||||
|
${up_total || 'N/A'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="margin-top:18px;">
|
||||||
|
<p style="margin-bottom:6px;"><strong>Products included:</strong></p>
|
||||||
|
<ul style="padding-left:18px;margin:0;">
|
||||||
|
${productosHTML}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="text-align:center;margin-top:32px;">
|
||||||
|
<a href="https://hacienda.consultoria-as.com/"
|
||||||
|
style="background-color:#7a0c1e;color:#ffffff;
|
||||||
|
text-decoration:none;font-weight:bold;
|
||||||
|
padding:14px 28px;border-radius:6px;
|
||||||
|
display:inline-block;">
|
||||||
|
Review and approve spending
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
We recommend reviewing this application before the deadline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Financial management system · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>`,
|
||||||
|
};
|
||||||
|
if(needtoapprove== true){
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message,
|
||||||
|
upExpense: upExpense
|
||||||
|
});
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaxes = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM gettaxes()');
|
||||||
|
|
||||||
|
const taxes = result.rows.map(tx => ({
|
||||||
|
id_tax: tx.id_tax,
|
||||||
|
name_tax: tx.name_tax,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:taxes
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los Taxes.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateDMY = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleDateString('es-MX', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { getPendingAppExpenses,getApprovedAppExpenses,countpending,
|
||||||
|
getTotalApproved,mainSupplier,getReportExpenses,newExpense,
|
||||||
|
getReportPayments,getmonthlypayments,getRejectedappExpenses,
|
||||||
|
updateExpense,getExpense,getInfoExpense, getTaxes };
|
||||||
161
backend/hotel_hacienda/src/controllers/hotelp.controller.js
Normal file
161
backend/hotel_hacienda/src/controllers/hotelp.controller.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const totalrevenue = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM totalrevenue($1,$2) as status',[start_date,end_date]);
|
||||||
|
const totalrenueve = result.rows[0].status;
|
||||||
|
/*const totalrenueve = result.rows.map(re => ({
|
||||||
|
total: re.total
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:totalrenueve
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cogs = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM cogs($1,$2) as status',[start_date,end_date]);
|
||||||
|
const cogs = result.rows[0].status;
|
||||||
|
/*const cogs = result.rows.map(re => ({
|
||||||
|
cogs_total: re.cogs_total,
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:cogs
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const employeeshare = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM employeeshare($1,$2) as status ',[start_date,end_date]);
|
||||||
|
const employeeshare = result.rows[0].status;
|
||||||
|
/*const employeeshare = result.rows.map(re => ({
|
||||||
|
total_share: re.total_share,
|
||||||
|
}));*/
|
||||||
|
res.json({
|
||||||
|
data:employeeshare
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tips = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM tips($1,$2) as status',[start_date,end_date]);
|
||||||
|
const tips = result.rows[0].status;
|
||||||
|
/*const tips = result.rows.map(re => ({
|
||||||
|
tips: re.tips
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:tips
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const grossprofit = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM grossprofit($1,$2) as status',[start_date,end_date]);
|
||||||
|
const grossprofit = result.rows[0].status;
|
||||||
|
/*const grossprofit = result.rows.map(re => ({
|
||||||
|
grossprofit: re.grossprofit
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:grossprofit
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const weightedCategoriesCost = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM weighted_categories_cost($1,$2)',[start_date,end_date]);
|
||||||
|
const weightedCategoriesCost = result.rows.map(re => ({
|
||||||
|
id_expense_cat : re.id_expense_cat ,
|
||||||
|
category_name: re.category_name ,
|
||||||
|
spanish_name : re.spanish_name ,
|
||||||
|
total : re.total,
|
||||||
|
participation : re.participation
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:weightedCategoriesCost
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ebitda = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM ebitda($1,$2)', [start_date,end_date]);
|
||||||
|
const ebitda = result.rows.map(re => ({
|
||||||
|
expenses_total: re.expenses_total,
|
||||||
|
ebitda : re.ebitda
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:ebitda
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {ebitda,weightedCategoriesCost,grossprofit,totalrevenue,cogs,employeeshare,tips};
|
||||||
309
backend/hotel_hacienda/src/controllers/incomehrx.controller.js
Normal file
309
backend/hotel_hacienda/src/controllers/incomehrx.controller.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const axios = require('axios');
|
||||||
|
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
||||||
|
const getFacturas = async (initialDate, finalDate) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(process.env.FACTURAS_API_URL, {
|
||||||
|
params: {
|
||||||
|
issuerRfc: process.env.FACTURAS_ISSUER_RFC,
|
||||||
|
type: process.env.FACTURAS_TYPE,
|
||||||
|
initialDate: initialDate,
|
||||||
|
finalDate: finalDate
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.FACTURAS_API_TOKEN}`
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Error API Facturas:',
|
||||||
|
error.response?.data || error.message
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const addfacturas = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const startOfYear = new Date(now.getFullYear(), 0, 1, 0, 0, 0);
|
||||||
|
//console.log(startOfYear);
|
||||||
|
const formatDateTime = (date) =>
|
||||||
|
date.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
|
const FACTURAS_INITIAL_DATE = formatDateTime(startOfYear);
|
||||||
|
const FACTURAS_FINAL_DATE = formatDateTime(now);
|
||||||
|
console.log(FACTURAS_INITIAL_DATE);
|
||||||
|
console.log(FACTURAS_FINAL_DATE);
|
||||||
|
const facturas = await getFacturas(FACTURAS_INITIAL_DATE,FACTURAS_FINAL_DATE);
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT addhoruxdata($1::jsonb) as total',[JSON.stringify(facturas)]);
|
||||||
|
const total = result.rows[0].total;
|
||||||
|
res.json({
|
||||||
|
message: 'Facturas subidas a la base de datos',
|
||||||
|
total
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al sincronizar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getCategoryIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getcategoryincome()');
|
||||||
|
|
||||||
|
const categoriesIncome = result.rows.map(inc => ({
|
||||||
|
id_cat_income: inc.id_cat_income,
|
||||||
|
name_cat_income: inc.name_cat_income,
|
||||||
|
english_name: inc.english_name
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:categoriesIncome
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las categorias' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInvoiceIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getinvoiceincome()');
|
||||||
|
|
||||||
|
const invoiceIncome = result.rows.map(inc => ({
|
||||||
|
id_inv_income: inc.id_inv_income,
|
||||||
|
name_inv_income: inc.name_inv_income,
|
||||||
|
amount: inc.amount,
|
||||||
|
dateinvoice: inc.dateinvoice
|
||||||
|
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:invoiceIncome
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las facturas.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getaccountincome()');
|
||||||
|
|
||||||
|
const aacountIncome = result.rows.map(inc => ({
|
||||||
|
id_acc_income: inc.id_acc_income,
|
||||||
|
name_acc_income: inc.name_acc_income,
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:aacountIncome
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las cuentas' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIncomeHorux = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getincomehorux()');
|
||||||
|
|
||||||
|
const incomeHorux = result.rows.map(inc => ({
|
||||||
|
id_hrx_income: inc.id_hrx_income,
|
||||||
|
date_in: inc.date_in,
|
||||||
|
createddate: inc.createddate,
|
||||||
|
account_name: inc.account_name,
|
||||||
|
categories: inc.categories,
|
||||||
|
amount:inc.amount,
|
||||||
|
amountinvoice: inc.amountinvoice,
|
||||||
|
invoice: inc.invoice,
|
||||||
|
status_in: inc.status_in
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:incomeHorux
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los income' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const getTotalIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM gettotalincome() as total');
|
||||||
|
|
||||||
|
const totalIncome = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Total de income',
|
||||||
|
data: totalIncome
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los income' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const newIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {
|
||||||
|
account_id,
|
||||||
|
amount,
|
||||||
|
new_date,
|
||||||
|
newinvoice,
|
||||||
|
area_id,
|
||||||
|
categories,
|
||||||
|
|
||||||
|
} = req.body;
|
||||||
|
const categoriesJson = categories ? JSON.stringify(categories) : '[]';
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT newincome($1,$2,$3,$4,$5,$6::jsonb) AS status',[
|
||||||
|
account_id,amount,new_date,newinvoice,area_id,categoriesJson]);
|
||||||
|
const newIncome = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Income agregado correctamente",
|
||||||
|
new_expense: newIncome
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOneIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await pool.query('SELECT * FROM getoneincome($1)',[parseInt(id)]);
|
||||||
|
|
||||||
|
const incomeHorux = result.rows.map(inc => ({
|
||||||
|
id_hrx_income: inc.id_hrx_income,
|
||||||
|
date_in: inc.date_in,
|
||||||
|
account_id: inc.account_id,
|
||||||
|
amount:inc.amount,
|
||||||
|
invoice: inc.invoice,
|
||||||
|
categories: inc.categories
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:incomeHorux
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el income' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIncome = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {id} = req.params;
|
||||||
|
const {
|
||||||
|
account_id,
|
||||||
|
amount,
|
||||||
|
up_date,
|
||||||
|
up_invoice,
|
||||||
|
area_id,
|
||||||
|
categories
|
||||||
|
} = req.body;
|
||||||
|
const categoriesJson = categories ? JSON.stringify(categories) : '[]';
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT updateincome($1,$2,$3,$4,$5,$6,$7::jsonb) AS status',[
|
||||||
|
id,account_id,amount,up_date,up_invoice,area_id,categoriesJson]);
|
||||||
|
const newIncome = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Income actualizado correctamente",
|
||||||
|
new_expense: newIncome
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const getstripeservice = async (req, res) => {
|
||||||
|
try{
|
||||||
|
/*const paymentIntent = await stripe.paymentIntents.create({
|
||||||
|
amount: 4805510,
|
||||||
|
currency: 'usd',
|
||||||
|
payment_method: 'pm_card_visa',
|
||||||
|
confirm: true,
|
||||||
|
automatic_payment_methods: {
|
||||||
|
enabled: true,
|
||||||
|
allow_redirects: 'never'
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
const balanceTransactions = await stripe.transfers.list();
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT addstripedatav2($1::jsonb) AS inserted',
|
||||||
|
[JSON.stringify(balanceTransactions)]);
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron datos de stripe y se insertaron",
|
||||||
|
transactions: result.rows[0].inserted,
|
||||||
|
//data: balanceTransactions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
const addDemostripeData = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const transfer = await stripe.transfers.create({
|
||||||
|
amount: 1000, // centavos
|
||||||
|
currency: 'usd',
|
||||||
|
destination: 'acct_1Sm54nEzzXSLq34b',
|
||||||
|
transfer_group: 'ORDER_95',});
|
||||||
|
res.json({
|
||||||
|
message: 'Pago creado y confirmado',
|
||||||
|
transfer
|
||||||
|
});
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {getCategoryIncome,getInvoiceIncome,getAccountIncome,
|
||||||
|
getIncomeHorux,getTotalIncome,newIncome,getOneIncome,updateIncome,getFacturas,addfacturas,getstripeservice,
|
||||||
|
addDemostripeData};
|
||||||
514
backend/hotel_hacienda/src/controllers/incomes.controller.js
Normal file
514
backend/hotel_hacienda/src/controllers/incomes.controller.js
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const XLSX = require('xlsx');
|
||||||
|
const csv = require('csv-parser');
|
||||||
|
const getincomes = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {checkin_initial,checkin_final} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM getincomesv2($1,$2)',[checkin_initial,checkin_final]);
|
||||||
|
const incomereport = result.rows.map(re => ({
|
||||||
|
room: re.room,
|
||||||
|
occupation: re.occupation,
|
||||||
|
income_per_night: re.income_per_night,
|
||||||
|
total_income: re.total_income
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:incomereport
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSoftRestaurant = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
//const {checkin_initial,checkin_final} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM getdataproductsolds()',);
|
||||||
|
const incomereport = result.rows.map(re => ({
|
||||||
|
id_soldpro: re.id_soldpro,
|
||||||
|
clave: re.clave,
|
||||||
|
description: re.description,
|
||||||
|
group_sp: re.group_sp,
|
||||||
|
price: re.price,
|
||||||
|
quantity: re.quantity,
|
||||||
|
totalsale: re.totalsale,
|
||||||
|
cost_sp: re.cost_sp,
|
||||||
|
totalcost: re.totalcost,
|
||||||
|
salecost: re.salecost,
|
||||||
|
catalogprice: re.catalogprice,
|
||||||
|
totalsalecatalogprice: re.totalsalecatalogprice,
|
||||||
|
vat_rate: re.vat_rate
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:incomereport
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getdetallecheques = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
//const {checkin_initial,checkin_final} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM getchequesdetalles()',);
|
||||||
|
const incomereport = result.rows.map(re => ({
|
||||||
|
id_cheque: re.id_cheque,
|
||||||
|
folio: re.folio,
|
||||||
|
fecha: re.fecha,
|
||||||
|
descuento: re.descuento,
|
||||||
|
importe: re.importe,
|
||||||
|
cargo: re.cargo,
|
||||||
|
efectivo: re.efectivo,
|
||||||
|
tarjeta: re.tarjeta,
|
||||||
|
vales: re.vales,
|
||||||
|
propina: re.propina,
|
||||||
|
OTROS: re.OTROS,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:incomereport
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalincomes = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {checkin_initial,checkin_final} = req.body;
|
||||||
|
const result = await pool.query('SELECT totalincomesv2($1,$2) as total',[checkin_initial,checkin_final]);
|
||||||
|
const totalincomes = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos de las fechas seleccionadas",
|
||||||
|
data:totalincomes
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos totales.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const channelscards = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {checkin_initial,checkin_final} = req.body;
|
||||||
|
const result = await pool.query('SELECT * from channelscardsv2($1,$2)',[checkin_initial,checkin_final]);
|
||||||
|
const incomef = result.rows.map(re => ({
|
||||||
|
channel: re.channel,
|
||||||
|
total_channel: re.total_channel,
|
||||||
|
percentage: re.percentage,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Canal donde se obtuvieron más ingresos",
|
||||||
|
data:incomef
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadincomes = (req, res) => {
|
||||||
|
const {nombrearchivo} = req.body;
|
||||||
|
const CSV_PATH = `/home/Hotel/backend/hotel_hacienda/src/resources/littleHotelier/${nombrearchivo}`;
|
||||||
|
//const CSV_PATH = `C:/Users/otamegane/Desktop/Trabajo_free_lancer/reservations/${nombrearchivo}`;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(CSV_PATH)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: 'El archivo CSV no existe'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.createReadStream(CSV_PATH)
|
||||||
|
.pipe(csv())
|
||||||
|
.on('data', (row) => {
|
||||||
|
results.push(row);
|
||||||
|
})
|
||||||
|
.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT insertincomesandtempV2($1::jsonb) AS inserted',
|
||||||
|
[JSON.stringify(results)]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'CSV procesado e insertado en base de datos',
|
||||||
|
rows_read: results.length,
|
||||||
|
rows_inserted: result.rows[0].inserted
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error insertando en BD:', error);
|
||||||
|
res.status(500).json({ message: 'Error al insertar datos en BD' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
console.error('Error leyendo el CSV:', err);
|
||||||
|
res.status(500).json({ error: 'No se pudo leer el CSV' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*const loadsoftrestaurant = (req, res) => {
|
||||||
|
const {nombrearchivo} = req.body;
|
||||||
|
//const CSV_PATH = `/home/Hotel/backend/hotel_hacienda/src/resources/littleHotelier/${nombrearchivo}`;
|
||||||
|
const CSV_PATH = `C:/Users/otamegane/Desktop/Trabajo_free_lancer/reservations/${nombrearchivo}`;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(CSV_PATH)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: 'El archivo CSV no existe'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.createReadStream(CSV_PATH)
|
||||||
|
.pipe(csv())
|
||||||
|
.on('data', (row) => {
|
||||||
|
results.push(row);
|
||||||
|
})
|
||||||
|
.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT insertproductsale($1::jsonb) AS inserted',
|
||||||
|
[JSON.stringify(results)]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'CSV procesado e insertado en base de datos',
|
||||||
|
rows_read: results.length,
|
||||||
|
rows_inserted: result.rows[0].inserted
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error insertando en BD:', error);
|
||||||
|
res.status(500).json({ message: 'Error al insertar datos en BD' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
console.error('Error leyendo el CSV:', err);
|
||||||
|
res.status(500).json({ error: 'No se pudo leer el CSV' });
|
||||||
|
});
|
||||||
|
};*/
|
||||||
|
|
||||||
|
const loadsoftrestaurant = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { nombrearchivo } = req.body;
|
||||||
|
//const FILE_PATH = `C:/Users/otamegane/Desktop/Trabajo_free_lancer/reservations/${nombrearchivo}`;
|
||||||
|
const FILE_PATH = `/home/Hotel/backend/hotel_hacienda/src/resources/littleHotelier/${nombrearchivo}`;
|
||||||
|
if (!fs.existsSync(FILE_PATH)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: 'El archivo Excel no existe'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Se lee el archivo
|
||||||
|
const workbook = XLSX.readFile(FILE_PATH);
|
||||||
|
|
||||||
|
//Toma la primera hoja primera hoja
|
||||||
|
const sheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
|
//Se convierte a json
|
||||||
|
const results = XLSX.utils.sheet_to_json(worksheet, {
|
||||||
|
defval: null,
|
||||||
|
range: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
//console.log(results[0]);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: 'El archivo no contiene datos'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insertar en PostgreSQL
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT insertproductsale($1::jsonb) AS inserted',
|
||||||
|
[JSON.stringify(results)]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Excel procesado e insertado en base de datos',
|
||||||
|
rows_read: results.length,
|
||||||
|
rows_inserted: result.rows[0].inserted
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error procesando Excel:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'Error al procesar el archivo Excel'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadChequesDetalle = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { nombrearchivo } = req.body;
|
||||||
|
const FILE_PATH = `/home/Hotel/backend/hotel_hacienda/src/resources/littleHotelier/${nombrearchivo}`;
|
||||||
|
//const FILE_PATH = `C:/Users/otamegane/Desktop/Trabajo_free_lancer/reservations/${nombrearchivo}`;
|
||||||
|
|
||||||
|
if (!fs.existsSync(FILE_PATH)) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: 'El archivo Excel no existe'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Se lee el archivo
|
||||||
|
const workbook = XLSX.readFile(FILE_PATH);
|
||||||
|
|
||||||
|
//Toma la primera hoja primera hoja
|
||||||
|
const sheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
|
//Se convierte a json
|
||||||
|
const results = XLSX.utils.sheet_to_json(worksheet, {
|
||||||
|
defval: null,
|
||||||
|
range: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
//console.log(results[0]);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: 'El archivo no contiene datos'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insertar en PostgreSQL
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT insertchequesdetalles($1::jsonb) AS inserted',
|
||||||
|
[JSON.stringify(results)]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Excel procesado e insertado en base de datos',
|
||||||
|
rows_read: results.length,
|
||||||
|
rows_inserted: result.rows[0].inserted
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error procesando Excel:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'Error al procesar el archivo Excel'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const reportIncomes = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * from stadisticsrooms($1,$2)',[initial_date,final_date]);
|
||||||
|
const reportincome = result.rows.map(re => ({
|
||||||
|
room_type: re.room_type,
|
||||||
|
total_uses: re.total_uses,
|
||||||
|
total_income: re.total_income,
|
||||||
|
occupation_percentage: re.occupation_percentage,
|
||||||
|
income_percentage: re.income_percentage,
|
||||||
|
ADR: re.adr,
|
||||||
|
CPOR: re.cpor,
|
||||||
|
CPAR: re.cpar,
|
||||||
|
PPOR: re.ppor,
|
||||||
|
fixed_cost: re.fixed_cost,
|
||||||
|
break_event_point: re.break_event_point,
|
||||||
|
gift_room: re.gift_room
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Reporte del income",
|
||||||
|
data:reportincome
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato',
|
||||||
|
error});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const countFolios = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT countFolios($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const count = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el conteo de los tickets",
|
||||||
|
data:count
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos totales.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sumasTotales = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT sumasTotales($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const sumatotal = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el total",
|
||||||
|
data:sumatotal
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos totales.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sumaEfectivo = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT sumaEfectivo($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const efectivo = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el total de efectivo",
|
||||||
|
data:efectivo
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos efectivo.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sumaTarjeta = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT sumaTarjeta($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const tarjeta = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el total de tarjeta",
|
||||||
|
data:tarjeta
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos tarjeta.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sumaVales = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT sumaVales($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const vales = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el total de vales",
|
||||||
|
data:vales
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos vales.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sumaOtros = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT sumaOtros($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const otros = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el total de otros",
|
||||||
|
data:otros
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos otros.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sumaPropinas = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT sumaPropinas($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const propinas = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el total de propinas",
|
||||||
|
data:propinas
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los ingresos propinas.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ticketPromedio = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {initial_date,final_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT ticketPromedio($1,$2) as total',[initial_date,final_date]);
|
||||||
|
const promedio = result.rows[0].total;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el promedio de ticket",
|
||||||
|
data:promedio
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el promedio.' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {getincomes,totalincomes,loadChequesDetalle,getdetallecheques,
|
||||||
|
channelscards,loadincomes,reportIncomes,loadsoftrestaurant,getSoftRestaurant,
|
||||||
|
ticketPromedio,sumaEfectivo,sumaOtros,sumaPropinas,sumaTarjeta,sumaVales,sumasTotales,countFolios};
|
||||||
650
backend/hotel_hacienda/src/controllers/mail.controller.js
Normal file
650
backend/hotel_hacienda/src/controllers/mail.controller.js
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
const transporter = require('../services/mailService');
|
||||||
|
|
||||||
|
const contractsNearToEnd = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const countResult = await pool.query('SELECT contratcsneartoend() as total');
|
||||||
|
const contractsResult = await pool.query('SELECT * FROM contractsneartoenddata()');
|
||||||
|
const userEmails = await pool.query('SELECT * FROM getusersmailneartoend()');
|
||||||
|
const totalExpiring = countResult.rows[0].total;
|
||||||
|
const contracts = contractsResult.rows;
|
||||||
|
const users = userEmails.rows;
|
||||||
|
|
||||||
|
if(totalExpiring >= 1 && users.length > 0){
|
||||||
|
|
||||||
|
const recipients = users.map(u => u.email);
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: 'Contratos próximos a expirar',
|
||||||
|
html: `
|
||||||
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
Notificación de contratos
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
Actualmente hay <b>${totalExpiring}</b> contrato(s) por expirar.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
Los siguientes contratos son los que expiran:
|
||||||
|
</p>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color:#f4f4f4;">
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">ID contract</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">RFC</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Nombre</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Fecha inicio contrato</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Fecha fin contrato</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Salario diario</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${contracts.map(c => {
|
||||||
|
const inicio = formatDateDMY(c.start_contract);
|
||||||
|
const fin = formatDateDMY(c.end_contract);
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.id_contract}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.rfc_emp}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.name_employee}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${inicio}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${fin}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.dailypay}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="text-align:center;margin-top:32px;">
|
||||||
|
<a href="https://hacienda.consultoria-as.com/"
|
||||||
|
style="background-color:#7a0c1e;color:#ffffff;
|
||||||
|
text-decoration:none;font-weight:bold;
|
||||||
|
padding:14px 28px;border-radius:6px;
|
||||||
|
display:inline-block;">
|
||||||
|
Revisar en la web
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
Por favor considere si seran recontratados o finalizados.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Sistema de gestión financiera · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con contratos por expirar.',
|
||||||
|
data:totalExpiring
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'No hay contratos por expirar',
|
||||||
|
data:totalExpiring
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const paymentDeadLine = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const deadLineCount = await pool.query('SELECT countdelaypayments() as total');
|
||||||
|
const deadLineResult = await pool.query('SELECT * FROM getdelaypayments()');
|
||||||
|
const usersDeadLine = await pool.query('SELECT * FROM getusersmailpaymentdelay()');
|
||||||
|
const totalDeadline = deadLineCount.rows[0].total;
|
||||||
|
const deadLines = deadLineResult.rows;
|
||||||
|
const users = usersDeadLine.rows;
|
||||||
|
|
||||||
|
if(totalDeadline >= 1 && users.length >0){
|
||||||
|
const recipients = users.map(u => u.email);
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: 'WARNING: OVERDUE PAYMENTS',
|
||||||
|
html: `
|
||||||
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
Notification regarding overdue payments
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
Currently there are <b>${totalDeadline}</b> overdue payment(s).
|
||||||
|
</p>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color:#f4f4f4;">
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">ID</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Description</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Deadline</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${deadLines.map(c => {
|
||||||
|
const fecha = new Date(c.payment_deadline);
|
||||||
|
const fechaFormateada = fecha.toLocaleDateString('en-US', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long'
|
||||||
|
});
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.id_expense}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.description}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${fechaFormateada}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.total || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="text-align:center;margin-top:32px;">
|
||||||
|
<a href="https://hacienda.consultoria-as.com/"
|
||||||
|
style="background-color:#7a0c1e;color:#ffffff;
|
||||||
|
text-decoration:none;font-weight:bold;
|
||||||
|
padding:14px 28px;border-radius:6px;
|
||||||
|
display:inline-block;">
|
||||||
|
Please review and pay as soon as possible.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
We recommend that review your overdue expenses as soon as possible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Financial management system · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>`
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con pagos atrasados.',
|
||||||
|
data:totalDeadline
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'No hay pagos con atraso',
|
||||||
|
data:totalDeadline
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con con los pagos atrasados' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const expensesNearToDeadline = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const deadLineCount = await pool.query('SELECT COUNT(*) FROM expensesneartodeadline()');
|
||||||
|
const deadLineResult = await pool.query('SELECT * FROM expensesneartodeadline()');
|
||||||
|
const usersDeadLine = await pool.query('SELECT * FROM getusersmailpaymentdelay()');
|
||||||
|
|
||||||
|
const totalDeadline = deadLineCount.rows[0].count;
|
||||||
|
const deadLines = deadLineResult.rows;
|
||||||
|
const users = usersDeadLine.rows;
|
||||||
|
console.log(totalDeadline);
|
||||||
|
if(totalDeadline >= 1 && users.length >0){
|
||||||
|
const recipients = users.map(u => u.email);
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: 'WARNING: PAYMENTS DUE SOON FOR LATE PAYMENTS',
|
||||||
|
html: `
|
||||||
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
Notification of payments approaching a deadline
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
Currently there are <b>${totalDeadline}</b> payment(s) close to deadline.
|
||||||
|
</p>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color:#f4f4f4;">
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">ID</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Description</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Deadline</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${deadLines.map(c => {
|
||||||
|
const fecha = new Date(c.payment_deadline);
|
||||||
|
const fechaFormateada = fecha.toLocaleDateString('en-US', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long'
|
||||||
|
});
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.id_expense}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.description}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${fechaFormateada}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.total || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="text-align:center;margin-top:32px;">
|
||||||
|
<a href="https://hacienda.consultoria-as.com/"
|
||||||
|
style="background-color:#7a0c1e;color:#ffffff;
|
||||||
|
text-decoration:none;font-weight:bold;
|
||||||
|
padding:14px 28px;border-radius:6px;
|
||||||
|
display:inline-block;">
|
||||||
|
Review and pay spendings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
We recommend reviewing this application before the deadline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Financial management system · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con pagos por expirar.',
|
||||||
|
data:totalDeadline
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'No hay pagos con atraso',
|
||||||
|
data:totalDeadline
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los pagos prontos a fecha limite' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const expensesspecial = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const expensesCount = await pool.query('SELECT COUNT(*) AS total FROM getexpensesspecial()');
|
||||||
|
const expensesResult = await pool.query('SELECT * FROM getexpensesspecial()');
|
||||||
|
const totalExpenses = expensesCount.rows[0].total;
|
||||||
|
const expenses = expensesResult.rows;
|
||||||
|
|
||||||
|
if(totalExpenses >= 1){
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: 'soporte@horuxfin.com',
|
||||||
|
subject: 'Recordatorio: Hay gastos por aprobar',
|
||||||
|
html: `
|
||||||
|
<h2>Notificación de gastos por aprobar</h2>
|
||||||
|
<p>Actualmente hay <b>${totalExpenses}</b> gasto(s) por aprobar</p>
|
||||||
|
<ul>Los siguientes gastos faltan por aprobar</ul>
|
||||||
|
<ul>
|
||||||
|
${expenses.map(c => {
|
||||||
|
const fecha = new Date(c.payment_deadline);
|
||||||
|
const fechaFormateada = fecha.toLocaleDateString('es-MX');
|
||||||
|
const product = Array.isArray(c.products) && c.products.length > 0
|
||||||
|
? c.products.map(p => `${p.name_product} ($${p.total})`).join(', ')
|
||||||
|
: 'N/A';
|
||||||
|
return `<li>
|
||||||
|
ID:${c.id_expense} - Descripción: ${c.description}
|
||||||
|
- Productos: ${product} -
|
||||||
|
fecha limite de pago: ${fechaFormateada} -
|
||||||
|
total del gasto: ${c.total || 'N/A'}
|
||||||
|
</li>`;
|
||||||
|
}).join('')}
|
||||||
|
</ul>`
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con gastos por aprobar',
|
||||||
|
data:totalExpenses
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'No hay pagos por aprobar',
|
||||||
|
data:totalExpenses
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailbirthday = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const endpointName = 'emailbirthday';
|
||||||
|
const current = new Date();
|
||||||
|
const mes = current.getMonth() + 1;
|
||||||
|
const anio = current.getFullYear();
|
||||||
|
const validateEndpoint = await pool.query('SELECT validateendpoint($1,$2::int,$3::int) AS total',[endpointName,mes,anio]);
|
||||||
|
|
||||||
|
if (validateEndpoint.rows[0].total > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: `Este endpoint ya fue ejecutado en ${current.toLocaleString('es-MX', { month: 'long' })}.`
|
||||||
|
});
|
||||||
|
}else {
|
||||||
|
await pool.query(`INSERT INTO endpoint_logs (endpoint_name) VALUES ($1)`,[endpointName]);
|
||||||
|
const birthdayCount = await pool.query('SELECT COUNT(*) AS total FROM getbirthdaysemployees()');
|
||||||
|
const birthdayResult = await pool.query('SELECT * FROM getbirthdaysemployees()');
|
||||||
|
const userEmails = await pool.query('SELECT * FROM getusersmailbirthday()');
|
||||||
|
|
||||||
|
const totalBirthday = birthdayCount.rows[0].total;
|
||||||
|
const birthdays = birthdayResult.rows;
|
||||||
|
const users = userEmails.rows;
|
||||||
|
const currentMonth = new Date().toLocaleString('en-US', { month: 'long' });
|
||||||
|
|
||||||
|
if(totalBirthday >= 1 && users.length >0){
|
||||||
|
|
||||||
|
const recipients = users.map(u => u.email);
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: `Friendly reminder: Birthdays of the month of ${currentMonth}`,
|
||||||
|
html: `
|
||||||
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
In this month there are ${totalBirthday} birthday person(s)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
The following employees are celebrating their birthdays this month:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color:#f4f4f4;">
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">RFC</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Name</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Birthday</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${birthdays.map(c => {
|
||||||
|
const fecha = new Date(c.birthday);
|
||||||
|
const fechaFormateada = fecha.toLocaleDateString('en-US', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long'
|
||||||
|
});
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.rfc}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.fullname}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${fechaFormateada}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
Remember to congratulate them on their special day!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Financial management system · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>`};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con los cumpleaños',
|
||||||
|
data:totalBirthday
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'No hay cumpleañeros este mes',
|
||||||
|
data:totalBirthday
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo enviar correo con los cumpleañeros' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const expiredcontracts = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const expiredCountTotal = await pool.query('SELECT expiredcontracts() as total');
|
||||||
|
const expiredResult = await pool.query('SELECT * FROM getexpirecontracts()');
|
||||||
|
const expiredCountDay = await pool.query('SELECT COUNT(*) AS total FROM getexpirecontracts()');
|
||||||
|
const totalExpired = expiredCountTotal.rows[0].total;
|
||||||
|
const totalDayExpired = expiredCountDay.rows[0].total;
|
||||||
|
const expiredContracts = expiredResult.rows;
|
||||||
|
|
||||||
|
if(totalExpired >= 1){
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: 'soporte@horuxfin.com',
|
||||||
|
subject: 'Contratos expirados el día de hoy',
|
||||||
|
html: `
|
||||||
|
<h2>Notificación contratos expirados el día de hoy</h2>
|
||||||
|
<p>Cantidad de contratos <b>${totalDayExpired}</b> que expiraron hoy.</p>
|
||||||
|
<ul>Los contratos expirados son</ul>
|
||||||
|
<ul>
|
||||||
|
${expiredContracts.map(c => {
|
||||||
|
const fecha = new Date(c.end_contract);
|
||||||
|
const fechaFormateada = fecha.toLocaleDateString('es-MX');
|
||||||
|
return `<li>
|
||||||
|
ID:${c.id_contract} - RFC: ${c.rfc_employee}
|
||||||
|
- Nombre: ${c.name_employee} -
|
||||||
|
fecha de expiración: ${fechaFormateada}
|
||||||
|
</li>`;
|
||||||
|
}).join('')}
|
||||||
|
</ul>
|
||||||
|
<p>Por favor considere si seran recontratados o no para que se inicie el proceso.</p>`
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con contratos expirados hoy',
|
||||||
|
data:totalDayExpired
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'Hoy no expiro ningun contrato',
|
||||||
|
data:totalDayExpired
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const expiredContractsMonth = async (req, res) => {
|
||||||
|
try{
|
||||||
|
// Verifica si hoy es el último día del mes
|
||||||
|
if (isLastDayOfMonth()) {
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'El proceso solo se ejecuta el último día del mes',
|
||||||
|
executed: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const endpointName = 'expiredContractsMonth';
|
||||||
|
const current = new Date();
|
||||||
|
const mes = current.getMonth() + 1;
|
||||||
|
const anio = current.getFullYear();
|
||||||
|
const validateEndpoint = await pool.query('SELECT validateendpoint($1,$2::int,$3::int) AS total',[endpointName,mes,anio]);
|
||||||
|
if (validateEndpoint.rows[0].total > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: `Este endpoint ya fue ejecutado en ${current.toLocaleString('es-MX', { month: 'long' })}.`
|
||||||
|
});
|
||||||
|
}else
|
||||||
|
{
|
||||||
|
await pool.query(`INSERT INTO endpoint_logs (endpoint_name) VALUES ($1)`,[endpointName]);
|
||||||
|
const expiredCountTotal = await pool.query('SELECT COUNT(*) AS total FROM getexpirecontractsmonth()');
|
||||||
|
const expiredResult = await pool.query('SELECT * FROM getexpirecontractsmonth()');
|
||||||
|
const userEmails = await pool.query('SELECT * FROM getusersmailneartoend()');
|
||||||
|
|
||||||
|
const totalExpired = expiredCountTotal.rows[0].total;
|
||||||
|
const expiredContracts = expiredResult.rows;
|
||||||
|
const currentMonth = new Date().toLocaleString('es-MX', { month: 'long' });
|
||||||
|
const users = userEmails.rows;
|
||||||
|
|
||||||
|
if(totalExpired >= 1 && users.length > 0){
|
||||||
|
const recipients = users.map(u => u.email);
|
||||||
|
const mailOptions = {
|
||||||
|
from: 'soporte@horuxfin.com',
|
||||||
|
to: recipients,
|
||||||
|
subject: `Contratos expirados en el mes ${currentMonth} `,
|
||||||
|
html: `
|
||||||
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
|
||||||
|
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="background-color:#7a0c1e;padding:18px 22px;">
|
||||||
|
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
|
||||||
|
Notificación contratos expirados del mes de ${currentMonth}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
En este mes expiraron <b>${totalExpired}.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0;">
|
||||||
|
Los contratos expirados son en el mes fueron:
|
||||||
|
</p>
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color:#f4f4f4;">
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">ID contract</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">RFC</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Nombre</th>
|
||||||
|
<th align="left" style="padding:8px;border:1px solid #ddd;">Fecha que expiro:</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${expiredContracts.map(c => {
|
||||||
|
const fin = formatDateDMY(c.end_contract);
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.id_contract}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.rfc_employee}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${c.name_employee}</td>
|
||||||
|
<td style="padding:8px;border:1px solid #ddd;">${fin}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="text-align:center;margin-top:32px;">
|
||||||
|
<a href="https://hacienda.consultoria-as.com/"
|
||||||
|
style="background-color:#7a0c1e;color:#ffffff;
|
||||||
|
text-decoration:none;font-weight:bold;
|
||||||
|
padding:14px 28px;border-radius:6px;
|
||||||
|
display:inline-block;">
|
||||||
|
Revisar en la web
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
|
||||||
|
Le recomendamos generar el contrato en caso de recontratación.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div style="background-color:#f6f6f6;padding:12px 20px;
|
||||||
|
font-size:12px;color:#888888;text-align:center;">
|
||||||
|
Sistema de gestión financiera · Consultoría AS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>`
|
||||||
|
};
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
res.json({
|
||||||
|
message: 'Correo enviado con contratos expirados en el mes',
|
||||||
|
data:totalExpired
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: 'Este mes no expiro ningun contrato',
|
||||||
|
data:totalExpired
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const isLastDayOfMonth = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const tomorrow = new Date(today);
|
||||||
|
tomorrow.setDate(today.getDate() + 1);
|
||||||
|
return tomorrow.getDate() === 1;
|
||||||
|
};
|
||||||
|
const formatDateDMY = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (isNaN(date.getTime())) return '';
|
||||||
|
return date.toLocaleDateString('es-MX', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {contractsNearToEnd,paymentDeadLine,expensesspecial,
|
||||||
|
emailbirthday,expiredcontracts,expiredContractsMonth,expensesNearToDeadline};
|
||||||
174
backend/hotel_hacienda/src/controllers/payment.controller.js
Normal file
174
backend/hotel_hacienda/src/controllers/payment.controller.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const newMonthlyExpense = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {
|
||||||
|
descriptionex,
|
||||||
|
recurrence_id,
|
||||||
|
payment_type,
|
||||||
|
currency_id,
|
||||||
|
suppliers_id,
|
||||||
|
area,
|
||||||
|
expense_category,
|
||||||
|
day_expense,
|
||||||
|
tax_id,
|
||||||
|
new_subtotal,
|
||||||
|
} = req.body;
|
||||||
|
const subtotal = new_subtotal && !isNaN(parseFloat(new_subtotal))? parseFloat(new_subtotal): 0;
|
||||||
|
const result = await pool.query('SELECT newmonthlyexpensev2($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) AS status',[
|
||||||
|
descriptionex,recurrence_id,payment_type,currency_id,suppliers_id,
|
||||||
|
area,expense_category,day_expense,tax_id,parseFloat(subtotal)]);
|
||||||
|
|
||||||
|
const newMonthly = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Gasto mensual agregado correctamente",
|
||||||
|
status: newMonthly
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se puede agregar el gasto mensual',error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const setPaymentStatusMonthly = async (req, res) => {
|
||||||
|
try{
|
||||||
|
|
||||||
|
const {id} = req.params;
|
||||||
|
const {
|
||||||
|
status_payment,
|
||||||
|
tax_id,
|
||||||
|
subtotal,
|
||||||
|
} = req.body;
|
||||||
|
const total = subtotal && subtotal !== '' ? subtotal : null;
|
||||||
|
if(total ==null)
|
||||||
|
{
|
||||||
|
const result = await pool.query('SELECT setpaymentstatusmonthlyv2($1,$2) as status',[id,status_payment]);
|
||||||
|
const statuspayment = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message:'Se actualizo el estatus del pago correctamente',
|
||||||
|
status_monthly: statuspayment
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
const result = await pool.query('SELECT setpaymentstatusmonthlyv2($1,$2,$3,$4) as status',[id,status_payment,tax_id,parseFloat(total)]);
|
||||||
|
const statuspayment = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message:'Se actualizo estatus y pago correctamente',
|
||||||
|
status_monthly: statuspayment
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo actualizar el pago' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshMonthlyExpenses = async(req,res) =>{
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT refresh_monthly_expenses() as status');
|
||||||
|
const refresh = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message:"se realizo actualizacion de gastos mensuales",
|
||||||
|
status_monthly: refresh
|
||||||
|
});
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo actualizar' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMonthlyExpense = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {
|
||||||
|
descriptionex,
|
||||||
|
recurrence_id,
|
||||||
|
payment_type,
|
||||||
|
currency_id,
|
||||||
|
suppliers_id,
|
||||||
|
area,
|
||||||
|
expense_category,
|
||||||
|
day_expense,
|
||||||
|
tax_id,
|
||||||
|
new_subtotal,
|
||||||
|
} = req.body;
|
||||||
|
const {id} = req.params;
|
||||||
|
const subtotal = new_subtotal && !isNaN(parseFloat(new_subtotal))? parseFloat(new_subtotal): 0;
|
||||||
|
const result = await pool.query('SELECT updatemonthlyexpensev2($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) AS status',
|
||||||
|
[id,descriptionex,recurrence_id,payment_type,currency_id,suppliers_id,
|
||||||
|
area,expense_category,day_expense,tax_id,parseFloat(subtotal)]);
|
||||||
|
|
||||||
|
const update = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Gasto mensual actualizado correctamente",
|
||||||
|
status: update
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se puede actualizar el gasto mensual' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getOneMonthly = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {id} = req.params;
|
||||||
|
const result = await pool.query('SELECT * FROM getONEmonthlypayment($1)',[id]);
|
||||||
|
|
||||||
|
const monthlypayment = result.rows.map(mp => ({
|
||||||
|
expense_id: mp.expense_id,
|
||||||
|
descriptionex: mp.descriptionex,
|
||||||
|
recurrence_id: mp.recurrence_id,
|
||||||
|
payment_type: mp.payment_type,
|
||||||
|
currency_idea: mp.currency_id,
|
||||||
|
suppliers_id: mp.suppliers_id,
|
||||||
|
area: mp.area,
|
||||||
|
expense_category: mp.expense_category,
|
||||||
|
day_expense: mp.day_expense,
|
||||||
|
tax_id: mp.tax_id,
|
||||||
|
new_subtotal: mp.new_subtotal
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:monthlypayment
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el pago mensual' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const needtorefresh = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {id} = req.params;
|
||||||
|
const {notrefresh} = req.body;
|
||||||
|
let mensaje;
|
||||||
|
const result = await pool.query('SELECT needToRefresh($1,$2)',[id,notrefresh]);
|
||||||
|
const rows = result.rowCount;
|
||||||
|
if (notrefresh == true)
|
||||||
|
mensaje = 'El gasto mensual se estara actualizando.';
|
||||||
|
else
|
||||||
|
mensaje = 'El gasto mensual NO se estara actualizando'
|
||||||
|
res.json({
|
||||||
|
data:rows,
|
||||||
|
message: mensaje
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo completar la accion' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {newMonthlyExpense,setPaymentStatusMonthly,refreshMonthlyExpenses,updateMonthlyExpense,getOneMonthly,needtorefresh};
|
||||||
513
backend/hotel_hacienda/src/controllers/product.controller.js
Normal file
513
backend/hotel_hacienda/src/controllers/product.controller.js
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const getproductsdisplay = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getproductsdisplay()'
|
||||||
|
);
|
||||||
|
const productsDisplay = result.rows.map(pro => ({
|
||||||
|
id_product:pro.id_product,
|
||||||
|
image_product: pro.image_product,
|
||||||
|
name_product: pro.name_product,
|
||||||
|
price_product: pro.price_product,
|
||||||
|
units: pro.units
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:productsDisplay
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los productos' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getproduct = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getoneproduct($1)',[parseInt(id)]);
|
||||||
|
const product = result.rows.map(pro => ({
|
||||||
|
id_product: pro.id_product,
|
||||||
|
image_product: pro.image_product,
|
||||||
|
name_product: pro.name_product,
|
||||||
|
sku_product: pro.sku_product,
|
||||||
|
supplier_product: pro.supplier_product,
|
||||||
|
price_product: pro.price_unit,
|
||||||
|
id_tax: pro.tax_product,
|
||||||
|
id_category_pro: pro.category_product,
|
||||||
|
product_type: pro.product_type,
|
||||||
|
stock: pro.stock,
|
||||||
|
id_curreny: pro.currency,
|
||||||
|
id_unit: pro.unit
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:product
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el producto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getcategoryproducts = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getcategoryproducts()'
|
||||||
|
);
|
||||||
|
const productCategory = result.rows.map(cat => ({
|
||||||
|
id_prod_category: cat.id_prod_category,
|
||||||
|
name_prod_category: cat.name_prod_category,
|
||||||
|
spanish_name: cat.spanish_name
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:productCategory
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener las categorias del producto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const gettypeproduct = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM gettypeproduct()'
|
||||||
|
);
|
||||||
|
const productType = result.rows.map(type => ({
|
||||||
|
id_product_type: type.id_product_type,
|
||||||
|
name_product_type: type.name_product_type,
|
||||||
|
spanish_name: type.spanish_name
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:productType
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los tipos del producto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const getsuppliers = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM getsuppliers()'
|
||||||
|
);
|
||||||
|
const suppliers = result.rows.map(supp => ({
|
||||||
|
name_suppliers: supp.name_suppliers,
|
||||||
|
rfc_suppliers: supp.rfc_suppliers,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:suppliers
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los proveedores del producto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const newsupplier = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {new_name_supp,
|
||||||
|
new_rfc_supp,
|
||||||
|
new_mail_supp,
|
||||||
|
new_phone_supp} = req.body
|
||||||
|
const result = await pool.query('SELECT newsupplier($1,$2,$3,$4) AS status',
|
||||||
|
[new_name_supp,new_rfc_supp, new_mail_supp, new_phone_supp]);
|
||||||
|
const newsupplier = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Proveedor agregado correctamente",
|
||||||
|
new_supplier: newsupplier
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al ingresar un nuevo proveedor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSupplier = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {id} = req.params;
|
||||||
|
const {
|
||||||
|
name_supp,
|
||||||
|
rfc_supp,
|
||||||
|
mail_supp ,
|
||||||
|
phone_supp} = req.body
|
||||||
|
const result = await pool.query('SELECT updateSupplier($1,$2,$3,$4) AS status',
|
||||||
|
[parseInt(id),name_supp,rfc_supp, mail_supp, phone_supp]);
|
||||||
|
const updatesupplier = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Proveedor actualizado",
|
||||||
|
new_supplier: updatesupplier
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al actualizar el proveedor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledSupplier = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {supplier_id} = req.body
|
||||||
|
const result = await pool.query('SELECT disableSuppliers($1) AS status', [supplier_id]);
|
||||||
|
const disabled = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Proveedor deshabilitado",
|
||||||
|
new_supplier: disabled
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al deshabilitar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const newproduct = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {new_name_product,
|
||||||
|
new_sku_product,
|
||||||
|
product_type,
|
||||||
|
new_category,
|
||||||
|
suppliers_id,
|
||||||
|
unit,
|
||||||
|
new_stock,
|
||||||
|
newprice_product,
|
||||||
|
new_tax,
|
||||||
|
currency,
|
||||||
|
image_product} = req.body
|
||||||
|
const result = await pool.query('SELECT newproduct($1,$2,$3::jsonb,$4,$5,$6,$7,$8,$9,$10,$11) AS status',
|
||||||
|
[new_name_product,new_sku_product,JSON.stringify(product_type),new_category,suppliers_id,unit,new_stock,
|
||||||
|
newprice_product,new_tax,currency,image_product]);
|
||||||
|
const newproduct = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Producto agregado correctamente",
|
||||||
|
new_product: newproduct
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al ingresar un nuevo proveedor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateproduct = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const { id } = req.params;
|
||||||
|
const {
|
||||||
|
product_type,
|
||||||
|
up_name_product,
|
||||||
|
up_sku_product,
|
||||||
|
suppliers_id,
|
||||||
|
up_price_product,
|
||||||
|
up_id_tax,
|
||||||
|
id_category,
|
||||||
|
new_stock,
|
||||||
|
img_product,
|
||||||
|
currency,
|
||||||
|
unit
|
||||||
|
} = req.body
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT updateproduct($1,$2::jsonb,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) AS status',
|
||||||
|
[parseInt(id),JSON.stringify(product_type),up_name_product,up_sku_product,suppliers_id,
|
||||||
|
up_price_product,parseInt(up_id_tax),parseInt(id_category),parseInt(new_stock),img_product,currency,unit]);
|
||||||
|
|
||||||
|
const updateProduct = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Se actualizo el producto correctamente",
|
||||||
|
new_product: updateProduct
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo actualizar el usuario'});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const discardProduct = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const { id } = req.params;
|
||||||
|
const { quantity,
|
||||||
|
reasons} = req.body
|
||||||
|
|
||||||
|
const result = await pool.query('SELECT discardproductsstock($1,$2,$3) AS status',
|
||||||
|
[parseInt(id),quantity,reasons]);
|
||||||
|
|
||||||
|
const discardProduct = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: "Producto descartado correctamente",
|
||||||
|
discard_product: discardProduct
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo hacer el descarte del producto'});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
const getdiscardproduct = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getdiscardproduct()');
|
||||||
|
|
||||||
|
const discartProducts = result.rows.map(dp => ({
|
||||||
|
id_discard: dp.id_discard,
|
||||||
|
product: dp.product,
|
||||||
|
original: dp.original,
|
||||||
|
quantity: dp.quantity,
|
||||||
|
reasons: dp.reasons,
|
||||||
|
date_discard : dp.date_dis,
|
||||||
|
total: dp.total
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los productos descartados",
|
||||||
|
product: discartProducts
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo hacer el descarte del producto'});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
const reportInventory = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM report_inventoryv2()');
|
||||||
|
|
||||||
|
const reportInventory = result.rows.map(ri => ({
|
||||||
|
date_movement: ri.date_movement,
|
||||||
|
id_product: ri.id_product,
|
||||||
|
name_product: ri.name_product,
|
||||||
|
category_en: ri.category_en,
|
||||||
|
category_esp:ri.category_esp,
|
||||||
|
movement: ri.movement,
|
||||||
|
comments_discard: ri.comments_discard,
|
||||||
|
housekeeper_name: ri.housekepper_name,
|
||||||
|
quantity: ri.quantity,
|
||||||
|
average_cost: ri.average_cost,
|
||||||
|
before_stock: ri.before_stock,
|
||||||
|
stock : ri.stock,
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el reporte de inventario",
|
||||||
|
new_product: reportInventory
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo obtener el reporte'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getstockadjusments = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getstockadjusments()');
|
||||||
|
|
||||||
|
const productAdjusment = result.rows.map(ri => ({
|
||||||
|
id_product: ri.id_product,
|
||||||
|
name_product: ri.name_product,
|
||||||
|
category_en: ri.category_en,
|
||||||
|
category_esp: ri.category_esp,
|
||||||
|
stock: ri.stock,
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el reporte de inventario",
|
||||||
|
productAdjusment: productAdjusment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo obtener el reporte'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setstockadjusments = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const { stockproductadjusment} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM setstockadjusmentsv2($1::jsonb) as stockadjus',[JSON.stringify(stockproductadjusment)]);
|
||||||
|
|
||||||
|
const stockproduct = result.rows[0].stockadjus;
|
||||||
|
res.json({
|
||||||
|
message: "Se hicieron los ajustes correspondientes",
|
||||||
|
stockproduct: stockproduct});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo realizar el ajuste'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getHouseKeeper = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM gethousekeeper()');
|
||||||
|
const houseKeepers = result.rows.map(hk => ({
|
||||||
|
rfc_employee: hk.rfc_employee,
|
||||||
|
name_emp: hk.name_emp,
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los datos",
|
||||||
|
houseKeeper: houseKeepers});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo obtener los datos'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newConsumptionStock = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {
|
||||||
|
product_id,
|
||||||
|
quantity_consumption,
|
||||||
|
date_consumption,
|
||||||
|
rfc_emp} = req.body
|
||||||
|
const result = await pool.query('SELECT consumptionstock($1,$2,$3,$4) as STATUS',[product_id,quantity_consumption,date_consumption,rfc_emp]);
|
||||||
|
const status = result.rows[0].status;
|
||||||
|
if(status == 1)
|
||||||
|
message = "El consumo del producto fue realizado con exito";
|
||||||
|
else
|
||||||
|
message = "No se pudo hacer el consumo"
|
||||||
|
res.json({
|
||||||
|
message,
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo obtener los datos'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getConsumptionStockReport = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getconsumptionreport()');
|
||||||
|
const stockConsumptions = result.rows.map(sc => ({
|
||||||
|
rfc_employee: sc.rfc_employee,
|
||||||
|
name_emp: sc.name_emp,
|
||||||
|
date_consumption:sc.date_consumption,
|
||||||
|
product_id:sc.product_id,
|
||||||
|
product_name:sc.product_name,
|
||||||
|
quantity:sc.quantity,
|
||||||
|
stocktotal:sc.stocktotal
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los datos",
|
||||||
|
stockConsumption: stockConsumptions});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo obtener los datos'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getproducts = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const page = parseInt(req.query.page) || 1; // Página actual
|
||||||
|
const limit = parseInt(req.query.limit) || 10; // Cantidad por página
|
||||||
|
const offset = (page - 1) * limit; // Desde dónde empezar
|
||||||
|
const result = await pool.query('SELECT * FROM getproducts() LIMIT $1 OFFSET $2',[limit, offset]);
|
||||||
|
|
||||||
|
const totalResult = await pool.query('SELECT COUNT(*) FROM products');
|
||||||
|
const total = parseInt(totalResult.rows[0].count);
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const products = result.rows.map(pro => ({
|
||||||
|
id_product: pro.id_product,
|
||||||
|
name_product:pro.name_product,
|
||||||
|
sku_product:pro.sku_product,
|
||||||
|
price_product:pro.price_product,
|
||||||
|
id_tax:pro.id_tax,
|
||||||
|
percentage: pro.percentage,
|
||||||
|
id_prod_category: pro.id_prod_category,
|
||||||
|
name_prod_category: pro.name_prod_category,
|
||||||
|
id_unit: pro.id_unit,
|
||||||
|
name_unit: pro.name_unit,
|
||||||
|
id_currency: pro.id_currency,
|
||||||
|
name_currency: pro.name_currency,
|
||||||
|
image_product: pro.image_product,
|
||||||
|
id_suppliers: pro.id_suppliers,
|
||||||
|
name_suppliers: pro.name_suppliers
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
message: "Se obtuvieron los datos",
|
||||||
|
product: products});
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo obtener los datos'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
{
|
||||||
|
getproductsdisplay,
|
||||||
|
getproducts,
|
||||||
|
getcategoryproducts,
|
||||||
|
gettypeproduct,
|
||||||
|
getsuppliers,
|
||||||
|
getproduct,
|
||||||
|
newsupplier,
|
||||||
|
newproduct,
|
||||||
|
updateproduct,
|
||||||
|
discardProduct,
|
||||||
|
getdiscardproduct,
|
||||||
|
reportInventory,
|
||||||
|
getstockadjusments,
|
||||||
|
setstockadjusments,
|
||||||
|
getHouseKeeper,
|
||||||
|
newConsumptionStock,
|
||||||
|
getConsumptionStockReport,
|
||||||
|
updateSupplier,
|
||||||
|
disabledSupplier
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
|
||||||
|
const getpurchases = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * From getpurchases()');
|
||||||
|
const purchases = result.rows.map(re => ({
|
||||||
|
id_purchase_dt: re.id_purchase_dt,
|
||||||
|
id_expense :re.id_expense ,
|
||||||
|
id_product :re.id_product ,
|
||||||
|
product_name: re.product_name,
|
||||||
|
quantity: re.quantity ,
|
||||||
|
delivered: re.delivered ,
|
||||||
|
id_tax :re.id_tax ,
|
||||||
|
total : re.total
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron todos los Purchases details",
|
||||||
|
data:purchases
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los purchases details.',error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const purchaseid = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {id} = req.params;
|
||||||
|
const {checking} = req.body;
|
||||||
|
|
||||||
|
console.log(id,checking);
|
||||||
|
const result = await pool.query('SELECT * FROM purchaseentry($1,$2) as status',[id,checking]);;
|
||||||
|
const newentry = result.rows[0].status;
|
||||||
|
res.json({
|
||||||
|
message: 'Se obtuvo el Purchases details', newentry
|
||||||
|
|
||||||
|
});
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el Purchases details',error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {getpurchases,purchaseid};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const getReportContracts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Leer query params con valores por defecto
|
||||||
|
const page = parseInt(req.query.page) || 1; // Página actual
|
||||||
|
const limit = parseInt(req.query.limit) || 10; // Cantidad por página
|
||||||
|
const offset = (page - 1) * limit; // Desde dónde empezar
|
||||||
|
|
||||||
|
// Llamamos a la función con LIMIT y OFFSET
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT * FROM reportcontracts() LIMIT $1 OFFSET $2`,
|
||||||
|
[limit, offset]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Obtener total para calcular páginas
|
||||||
|
const totalResult = await pool.query('SELECT COUNT(*) FROM contracts');
|
||||||
|
const total = parseInt(totalResult.rows[0].count);
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
const contracts = result.rows.map(con => ({
|
||||||
|
name_employee: con.id_contract,
|
||||||
|
position_employee: con.name_employee,
|
||||||
|
area_employee: con.position_employee,
|
||||||
|
contract_end: con.end_contract,
|
||||||
|
daily_pay: con.daily_pay,
|
||||||
|
status_contracts: con.status_contracts
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
data: contracts
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener los contratos' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { getReportContracts };
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
|
||||||
|
const getSharedExpenses = async (req, res) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = await pool.query(`SELECT * FROM shared_expenses_dashboard`);
|
||||||
|
|
||||||
|
const data = result.rows.map(row => ({
|
||||||
|
id_expense: row.id_expense,
|
||||||
|
id_properties: row.id_properties,
|
||||||
|
id_expense_type: row.id_expense_type,
|
||||||
|
request_date: row.request_date,
|
||||||
|
payment_deadline: row.payment_deadline,
|
||||||
|
id_area: row.id_area,
|
||||||
|
id_expense_cat: row.id_expense_cat,
|
||||||
|
id_approval_stat: row.id_approval_stat,
|
||||||
|
id_currency: row.id_currency,
|
||||||
|
is_purchase: row.is_purchase,
|
||||||
|
total: row.total,
|
||||||
|
subtotal: row.subtotal,
|
||||||
|
iva: row.iva,
|
||||||
|
ieps: row.ieps,
|
||||||
|
id_suppliers: row.id_suppliers,
|
||||||
|
id_payment_status: row.id_payment_status,
|
||||||
|
id_cat_percent: row.id_cat_percent,
|
||||||
|
participation: row.participation,
|
||||||
|
id_fix_variable: row.id_fix_variable
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ count: data.length, data });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Error al obtener los datos del dashboard' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { getSharedExpenses };
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const totalrevenue = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM totalrevenue_rest($1,$2) as status',[start_date,end_date]);
|
||||||
|
const totalrenueve = result.rows[0].status;
|
||||||
|
/*const totalrenueve = result.rows.map(re => ({
|
||||||
|
total: re.total
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
data:totalrenueve
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cogs = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM cogs_rest($1,$2) as status',[start_date,end_date]);
|
||||||
|
const cogs = result.rows[0].status;
|
||||||
|
/*const cogs = result.rows.map(re => ({
|
||||||
|
cogs_total: re.cogs_total,
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:cogs
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const grossprofit = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM grossprofit_rest($1,$2) as status',[start_date,end_date]);
|
||||||
|
const grossprofit = result.rows[0].status;
|
||||||
|
/*const grossprofit = result.rows.map(re => ({
|
||||||
|
grossprofit: re.grossprofit
|
||||||
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:grossprofit
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const weightedCategoriesCost = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body
|
||||||
|
const result = await pool.query('SELECT * FROM weighted_categories_rest_cost($1,$2)',[start_date,end_date]);
|
||||||
|
const weightedCategoriesCost = result.rows.map(re => ({
|
||||||
|
id_expense_cat : re.id_expense_cat ,
|
||||||
|
category_name: re.category_name ,
|
||||||
|
spanish_name : re.spanish_name ,
|
||||||
|
total : re.total,
|
||||||
|
participation : re.participation
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
data:weightedCategoriesCost
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ebitda = async (req, res) =>
|
||||||
|
{
|
||||||
|
try{
|
||||||
|
const {start_date,end_date} = req.body;
|
||||||
|
const result = await pool.query('SELECT * FROM ebitda_rest($1,$2)', [start_date,end_date]);
|
||||||
|
const ebitda = result.rows.map(re => ({
|
||||||
|
expenses_total: re.expenses_total,
|
||||||
|
ebitda : re.ebitda
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los ingresos",
|
||||||
|
data:ebitda
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener el dato' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {ebitda,weightedCategoriesCost,grossprofit,totalrevenue,cogs};
|
||||||
188
backend/hotel_hacienda/src/controllers/settings.controller.js
Normal file
188
backend/hotel_hacienda/src/controllers/settings.controller.js
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const addNewRoom = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {room,units,cost_per_nigth,guest,bed,amenities,products} = req.body;
|
||||||
|
|
||||||
|
const result = await pool.query(`SELECT new_room($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb) AS room_id`,
|
||||||
|
[room,units,cost_per_nigth,guest,bed,JSON.stringify(amenities),JSON.stringify(products)]);
|
||||||
|
const newRoom = result.rows[0].room_id;
|
||||||
|
res.json({
|
||||||
|
message: "Se agrego correctamente la habitacion",
|
||||||
|
new_room: newRoom
|
||||||
|
});
|
||||||
|
}catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo agregar nueva habitacion' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
const reportRoom = async(req,res) =>{
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM reportrooms()');
|
||||||
|
|
||||||
|
const rooms = result.rows.map(ro => ({
|
||||||
|
id_room: ro.id_room,
|
||||||
|
name_room: ro.name_room,
|
||||||
|
guests: ro.guests,
|
||||||
|
bed_type: ro.bed_type,
|
||||||
|
quantity: ro.quantity
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el reporte de habitaciones",
|
||||||
|
new_product: rooms
|
||||||
|
});
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No pudo recuperar la tabla de reporte.'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const addNewProperty = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const {new_name,addres_pro,main_area_pro,rfc_pro,phone,areas,services} = req.body;
|
||||||
|
|
||||||
|
const result = await pool.query(`SELECT new_property($1,$2,$3,$4,$5,$6::jsonb,$7::jsonb) AS property_id`,
|
||||||
|
[new_name,addres_pro,main_area_pro,rfc_pro,phone,JSON.stringify(areas),JSON.stringify(services)]);
|
||||||
|
const newRoom = result.rows[0].property_id;
|
||||||
|
res.json({
|
||||||
|
message: "Se agrego correctamente la propiedad",
|
||||||
|
new_property: newRoom
|
||||||
|
});
|
||||||
|
}catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo agregar nueva propiedad' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
const reportProperties = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM reportproperties()');
|
||||||
|
const property = result.rows.map(ro => ({
|
||||||
|
id_prop: ro.id_prop,
|
||||||
|
name_property: ro.name_property,
|
||||||
|
areas: ro.areas
|
||||||
|
}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvo el reporte de propiedades",
|
||||||
|
reportProperties: property
|
||||||
|
});
|
||||||
|
}catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo consultar las propiedades' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getunits = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getunits()');
|
||||||
|
const units = result.rows.map(u => ({
|
||||||
|
id_unit: u.id_unit,
|
||||||
|
name_unit: u.name_unit}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron las unidades de medida",
|
||||||
|
units: units
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo recuperar las unidades' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getapproveby = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getapproveby()');
|
||||||
|
const approveby = result.rows.map(ap => ({
|
||||||
|
id_user: ap.id_user,
|
||||||
|
user_name: ap.user_name}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron las aprobadores",
|
||||||
|
approve: approveby
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo recuperar los aprobadores' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getcurrency = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getcurrency()');
|
||||||
|
const currency = result.rows.map(u => ({
|
||||||
|
id_currency: u.id_currency,
|
||||||
|
name_currency: u.name_currency}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron las monedas",
|
||||||
|
currency: currency
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo recuperar las monedas' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const getrecurrence = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getrecurrence()');
|
||||||
|
const recurrence = result.rows.map(u => ({
|
||||||
|
id_recurrence: u.id_recurrence,
|
||||||
|
name_recurrence: u.name_recurrence}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron las recurrencias",
|
||||||
|
currency: recurrence
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo recuperar la recurrencia' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getrequestby = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getrequestby()');
|
||||||
|
const units = result.rows.map(u => ({
|
||||||
|
id_user: u.id_user,
|
||||||
|
user_name: u.user_name}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron los solicitantes",
|
||||||
|
request: units
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo recuperar los solicitantes' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getcategoryexpense = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT * FROM getcategoryexpense()');
|
||||||
|
const categoryex = result.rows.map(u => ({
|
||||||
|
id_category: u.id_category,
|
||||||
|
name_category: u.name_category}));
|
||||||
|
res.json({
|
||||||
|
message: "Se obtuvieron las categorias del gasto",
|
||||||
|
categoryex: categoryex
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'No se pudo las categorias del gasto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
module.exports = {addNewRoom,reportRoom,addNewProperty,reportProperties,getunits,
|
||||||
|
getcategoryexpense,getrequestby,getcurrency,getapproveby,getrecurrence};
|
||||||
107
backend/hotel_hacienda/src/controllers/status.controller.js
Normal file
107
backend/hotel_hacienda/src/controllers/status.controller.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const pool = require('../db/connection');
|
||||||
|
|
||||||
|
const setapprovalstatus = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const {status, approved_by} = req.body;
|
||||||
|
console.log('params:', id);
|
||||||
|
console.log('body:', status);
|
||||||
|
const result = await pool.query('SELECT setapprovalstatus($1,$2,$3) AS status',[id,status,approved_by]);
|
||||||
|
const updateapproval = result.rows[0].status;
|
||||||
|
if(updateapproval == 3)
|
||||||
|
{
|
||||||
|
res.json({
|
||||||
|
message: "El usuario no tiene permisos para aprobar.",
|
||||||
|
new_product: updateapproval
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
res.json({
|
||||||
|
message: "Se actualizo correctamente el estatus de aprobación",
|
||||||
|
new_product: updateapproval
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al actualizar el estatus de aprobacion' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const setpaymentstatus = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const {status} = req.body;
|
||||||
|
console.log('params:', id);
|
||||||
|
console.log('body:', status);
|
||||||
|
const result = await pool.query('SELECT setpaymentstatusv2($1,$2) AS status',[id,status]);
|
||||||
|
|
||||||
|
const updatepayment = result.rows[0].status;
|
||||||
|
if(updatepayment == -3){
|
||||||
|
res.json({
|
||||||
|
message: "El gasto no esta aprobado o fue rechazado",
|
||||||
|
paymentstatus: updatepayment
|
||||||
|
});}
|
||||||
|
else {
|
||||||
|
res.json({
|
||||||
|
message: "Se actualizo correctamente el estatus del pago",
|
||||||
|
paymentstatus: updatepayment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al actualizar el estatus del pago' });
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const pendingAppPayments = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT pendingapppayments() AS pendingapp',);
|
||||||
|
const pendingapprov = result.rows[0].pendingapp;
|
||||||
|
|
||||||
|
console.log(result.rows[0].pendingApp);
|
||||||
|
res.json({
|
||||||
|
message: "Pagos pendientes de aprobación",
|
||||||
|
data: pendingapprov
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener pagos pendientes por aprobar' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const countDelayPayments = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT countdelaypayments() AS delaypayments',);
|
||||||
|
const delayPay = result.rows[0].delaypayments;
|
||||||
|
res.json({
|
||||||
|
message: "Pagos con retraso",
|
||||||
|
data: delayPay
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener pagos con retraso' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const totalSpent = async (req, res) => {
|
||||||
|
try{
|
||||||
|
const result = await pool.query('SELECT totalspent() AS spent',);
|
||||||
|
const totalSpent = result.rows[0].spent;
|
||||||
|
res.json({
|
||||||
|
message: "Total gastado",
|
||||||
|
data: totalSpent
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ message: 'Error al obtener total gastado' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { setapprovalstatus,setpaymentstatus,pendingAppPayments,countDelayPayments,totalSpent };
|
||||||
16
backend/hotel_hacienda/src/db/connection.js
Normal file
16
backend/hotel_hacienda/src/db/connection.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
15
backend/hotel_hacienda/src/middlewares/handleValidation.js
Normal file
15
backend/hotel_hacienda/src/middlewares/handleValidation.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const { validationResult } = require('express-validator');
|
||||||
|
|
||||||
|
const handleValidation = (req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: 400,
|
||||||
|
message: 'Error en la validación de datos',
|
||||||
|
errors: errors.array()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = handleValidation;
|
||||||
12
backend/hotel_hacienda/src/middlewares/validators.js
Normal file
12
backend/hotel_hacienda/src/middlewares/validators.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const { body } = require('express-validator');
|
||||||
|
|
||||||
|
const loginValidator = [
|
||||||
|
body('name_mail_user')
|
||||||
|
.trim()
|
||||||
|
.notEmpty().withMessage('El correo o nombre de usuario es obligatorio'),
|
||||||
|
body('user_pass')
|
||||||
|
.trim()
|
||||||
|
.notEmpty().withMessage('La contraseña es obligatoria')
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = { loginValidator };
|
||||||
File diff suppressed because it is too large
Load Diff
11
backend/hotel_hacienda/src/routes/auth.routes.js
Normal file
11
backend/hotel_hacienda/src/routes/auth.routes.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const authController = require('../controllers/auth.controller');
|
||||||
|
const { loginValidator } = require('../middlewares/validators');
|
||||||
|
const handleValidation = require('../middlewares/handleValidation');
|
||||||
|
|
||||||
|
router.post('/login',loginValidator, handleValidation, authController.login);
|
||||||
|
router.post('/createuser',authController.createuser);
|
||||||
|
router.post('/recoverpass',authController.passRecover);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
16
backend/hotel_hacienda/src/routes/contract.routes.js
Normal file
16
backend/hotel_hacienda/src/routes/contract.routes.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const contractController = require('../controllers/contract.controller');
|
||||||
|
|
||||||
|
router.get('/', contractController.getContracts);
|
||||||
|
router.get('/getinfocontract/:id', contractController.getContract);
|
||||||
|
router.get('/neartoend', contractController.neartoend);
|
||||||
|
router.get('/positions', contractController.getpositions);
|
||||||
|
router.get('/areas', contractController.getareas);
|
||||||
|
router.get('/bosses', contractController.getbosses);
|
||||||
|
router.get('/reportempcontract', contractController.reportEmployeeContract);
|
||||||
|
router.get('/disabledcontract', contractController.disabledcontracts);
|
||||||
|
|
||||||
|
router.post('/newcontract',contractController.newContract);
|
||||||
|
router.put('/updatecontract/:id', contractController.updateContract);
|
||||||
|
module.exports = router;
|
||||||
14
backend/hotel_hacienda/src/routes/employee.routes.js
Normal file
14
backend/hotel_hacienda/src/routes/employee.routes.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const employeeController = require('../controllers/employee.controller');
|
||||||
|
|
||||||
|
router.get('/', employeeController.getEmployees);
|
||||||
|
router.get('/activeEmployees',employeeController.getTotalActiveEmployees);
|
||||||
|
router.get('/getattendance',employeeController.getattendance);
|
||||||
|
router.get('/gradeofstudy',employeeController.getGradeOfStudy);
|
||||||
|
router.get('/relationship',employeeController.getRelationshipEmployee);
|
||||||
|
router.post('/employee',employeeController.getEmployee);
|
||||||
|
router.post('/newemployee',employeeController.newEmployee);
|
||||||
|
router.post('/updateemployee',employeeController.updateEmployee);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
8
backend/hotel_hacienda/src/routes/exchange.routes.js
Normal file
8
backend/hotel_hacienda/src/routes/exchange.routes.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const exchangeRouter = require('../controllers/exchange.controller');
|
||||||
|
|
||||||
|
router.post('/consultexchange',exchangeRouter.consultExchange );
|
||||||
|
router.get('/getexchanges',exchangeRouter.getExchange );
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
20
backend/hotel_hacienda/src/routes/expense.routes.js
Normal file
20
backend/hotel_hacienda/src/routes/expense.routes.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const expenseController = require('../controllers/expense.controller');
|
||||||
|
|
||||||
|
|
||||||
|
router.get('/pendingapproval', expenseController.getPendingAppExpenses);
|
||||||
|
router.get('/approvedexpenses', expenseController.getApprovedAppExpenses);
|
||||||
|
router.get('/rejectedexpenses', expenseController.getRejectedappExpenses);
|
||||||
|
router.get('/mainsupplier',expenseController.mainSupplier);
|
||||||
|
router.put('/getexpense/:id',expenseController.getExpense);
|
||||||
|
router.get('/getinfo', expenseController.getInfoExpense);
|
||||||
|
router.get('/reportexpenses', expenseController.getReportExpenses);
|
||||||
|
router.post('/countpending',expenseController.countpending);
|
||||||
|
router.get('/reportpayments',expenseController.getReportPayments);
|
||||||
|
router.get('/monthlypayments',expenseController.getmonthlypayments);
|
||||||
|
router.post('/newexpense',expenseController.newExpense);
|
||||||
|
router.post('/totalapproved', expenseController.getTotalApproved);
|
||||||
|
router.put('/updateexpense/:id',expenseController.updateExpense);
|
||||||
|
router.get('/gettaxes',expenseController.getTaxes);
|
||||||
|
module.exports = router;
|
||||||
14
backend/hotel_hacienda/src/routes/hotelpl.routes.js
Normal file
14
backend/hotel_hacienda/src/routes/hotelpl.routes.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const hotelControllers = require('../controllers/hotelp.controller');
|
||||||
|
|
||||||
|
router.post('/cogs', hotelControllers.cogs);
|
||||||
|
router.post('/ebitda', hotelControllers.ebitda);
|
||||||
|
router.post('/employeeshare', hotelControllers.employeeshare);
|
||||||
|
router.post('/grossprofit', hotelControllers.grossprofit);
|
||||||
|
router.post('/tips', hotelControllers.tips);
|
||||||
|
router.post('/totalrevenue', hotelControllers.totalrevenue);
|
||||||
|
router.post('/weightedCategoriesCost', hotelControllers.weightedCategoriesCost);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
25
backend/hotel_hacienda/src/routes/incomehrx.routes.js
Normal file
25
backend/hotel_hacienda/src/routes/incomehrx.routes.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const incomeController = require('../controllers/incomehrx.controller');
|
||||||
|
|
||||||
|
router.get('/accountincome', incomeController.getAccountIncome);
|
||||||
|
router.get('/categoryincome', incomeController.getCategoryIncome);
|
||||||
|
router.get('/invoiceIncome', incomeController.getInvoiceIncome);
|
||||||
|
router.get('/totalIncome', incomeController.getTotalIncome);
|
||||||
|
router.get('/incomehorux', incomeController.getIncomeHorux);
|
||||||
|
router.get('/oneincomehorux/:id', incomeController.getOneIncome);
|
||||||
|
router.get('/stripedata/', incomeController.getstripeservice);
|
||||||
|
router.post('/stripedatademo/', incomeController.addDemostripeData);
|
||||||
|
router.post('/insertinvoice', incomeController.addfacturas);
|
||||||
|
router.post('/newincome', incomeController.newIncome);
|
||||||
|
router.put('/updateincome/:id', incomeController.updateIncome);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
34
backend/hotel_hacienda/src/routes/incomes.routes.js
Normal file
34
backend/hotel_hacienda/src/routes/incomes.routes.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const incomeController = require('../controllers/incomes.controller');
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/getincomes',incomeController.getincomes);
|
||||||
|
router.post('/totalincomes',incomeController.totalincomes);
|
||||||
|
router.post('/channelscards',incomeController.channelscards);
|
||||||
|
router.post('/loadincomes',incomeController.loadincomes);
|
||||||
|
router.post('/loadproductsales',incomeController.loadsoftrestaurant);
|
||||||
|
router.post('/loadchequesdetalle',incomeController.loadChequesDetalle);
|
||||||
|
router.post('/reportincomes',incomeController.reportIncomes);
|
||||||
|
router.post('/countticket',incomeController.countFolios);
|
||||||
|
router.post('/efectivo',incomeController.sumaEfectivo);
|
||||||
|
router.post('/otros',incomeController.sumaOtros);
|
||||||
|
router.post('/propinas',incomeController.sumaPropinas);
|
||||||
|
router.post('/tarjeta',incomeController.sumaTarjeta);
|
||||||
|
router.post('/vales',incomeController.sumaVales);
|
||||||
|
router.post('/sumatotal',incomeController.sumasTotales);
|
||||||
|
router.post('/ticketpromedio',incomeController.ticketPromedio);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
router.get('/getproductsales',incomeController.getSoftRestaurant);
|
||||||
|
router.get('/getdetallecheque',incomeController.getdetallecheques);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
12
backend/hotel_hacienda/src/routes/mail.routes.js
Normal file
12
backend/hotel_hacienda/src/routes/mail.routes.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const emailController = require('../controllers/mail.controller');
|
||||||
|
|
||||||
|
router.post('/nearexpiring', emailController.contractsNearToEnd);
|
||||||
|
router.post('/paymentdelay', emailController.paymentDeadLine);
|
||||||
|
router.post('/expensesneartodeadline', emailController.expensesNearToDeadline);
|
||||||
|
router.post('/expensesspecial',emailController.expensesspecial);
|
||||||
|
router.post('/birthdays',emailController.emailbirthday);
|
||||||
|
router.post('/contractexpired',emailController.expiredcontracts);
|
||||||
|
router.post('/expiredcontractsmonth',emailController.expiredContractsMonth);
|
||||||
|
module.exports = router;
|
||||||
11
backend/hotel_hacienda/src/routes/payment.routes.js
Normal file
11
backend/hotel_hacienda/src/routes/payment.routes.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const paymentController = require('../controllers/payment.controller');
|
||||||
|
|
||||||
|
router.post('/newexpmonthly', paymentController.newMonthlyExpense);
|
||||||
|
router.get('/refreshmonthly',paymentController.refreshMonthlyExpenses);
|
||||||
|
router.put('/paymentstatusmonthly/:id', paymentController.setPaymentStatusMonthly);
|
||||||
|
router.put('/updateexpmonthly/:id',paymentController.updateMonthlyExpense);
|
||||||
|
router.get('/onemothlyexpense/:id', paymentController.getOneMonthly);
|
||||||
|
router.put('/needtorefresh/:id', paymentController.needtorefresh);
|
||||||
|
module.exports = router;
|
||||||
27
backend/hotel_hacienda/src/routes/product.routes.js
Normal file
27
backend/hotel_hacienda/src/routes/product.routes.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const productController = require('../controllers/product.controller');
|
||||||
|
const { route } = require('./contract.routes');
|
||||||
|
|
||||||
|
router.get('/', productController.getproductsdisplay);
|
||||||
|
router.get('/productcategory', productController.getcategoryproducts);
|
||||||
|
router.get('/producttype', productController.gettypeproduct);
|
||||||
|
router.get('/suppliers',productController.getsuppliers);
|
||||||
|
router.get('/gdiscardproducts',productController.getdiscardproduct);
|
||||||
|
router.get('/reportinventory',productController.reportInventory);
|
||||||
|
router.get('/stockadjusment',productController.getstockadjusments);
|
||||||
|
router.get('/gethousekeeper',productController.getHouseKeeper);
|
||||||
|
router.get('/getproducts',productController.getproducts);
|
||||||
|
router.get('/getconsumptionstockreport',productController.getConsumptionStockReport);
|
||||||
|
|
||||||
|
router.post('/newsupplier',productController.newsupplier);
|
||||||
|
router.post('/newproduct',productController.newproduct);
|
||||||
|
router.post('/stockadjusmentset',productController.setstockadjusments);
|
||||||
|
router.post('/newconsumptionstock',productController.newConsumptionStock);
|
||||||
|
router.post('/disableSupplier',productController.disabledSupplier);
|
||||||
|
|
||||||
|
router.put('/update_product/:id',productController.updateproduct);
|
||||||
|
router.put('/discardproduct/:id',productController.discardProduct);
|
||||||
|
router.put('/product/:id',productController.getproduct);
|
||||||
|
router.put('updatesupplier/:id',productController.updateSupplier);
|
||||||
|
module.exports = router;
|
||||||
10
backend/hotel_hacienda/src/routes/purchase.routes.js
Normal file
10
backend/hotel_hacienda/src/routes/purchase.routes.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const purchaseController = require('../controllers/purchase.controller');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
router.get('/getpurchases',purchaseController.getpurchases);
|
||||||
|
router.put('/entry/:id',purchaseController.purchaseid);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const reportContractController = require('../controllers/reportcontract.controller');
|
||||||
|
|
||||||
|
router.get('/', reportContractController.getReportContracts);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
12
backend/hotel_hacienda/src/routes/restaurantpl.routes.js
Normal file
12
backend/hotel_hacienda/src/routes/restaurantpl.routes.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const restaurantControllers = require('../controllers/restaurantpl.controller');
|
||||||
|
|
||||||
|
router.post('/cogs', restaurantControllers.cogs);
|
||||||
|
router.post('/ebitda', restaurantControllers.ebitda);
|
||||||
|
router.post('/grossprofit', restaurantControllers.grossprofit);
|
||||||
|
router.post('/totalrevenue', restaurantControllers.totalrevenue);
|
||||||
|
router.post('/weightedCategoriesCost', restaurantControllers.weightedCategoriesCost);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
16
backend/hotel_hacienda/src/routes/settings.routes.js
Normal file
16
backend/hotel_hacienda/src/routes/settings.routes.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const settingsController = require('../controllers/settings.controller');
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/newroom', settingsController.addNewRoom);
|
||||||
|
router.post('/newproperty', settingsController.addNewProperty);
|
||||||
|
router.get('/reportrooms', settingsController.reportRoom);
|
||||||
|
router.get('/reportproperties', settingsController.reportProperties);
|
||||||
|
router.get('/approveby', settingsController.getapproveby);
|
||||||
|
router.get('/requestby', settingsController.getrequestby);
|
||||||
|
router.get('/categoryexpense', settingsController.getcategoryexpense);
|
||||||
|
router.get('/currency', settingsController.getcurrency);
|
||||||
|
router.get('/units', settingsController.getunits);
|
||||||
|
router.get('/recurrence', settingsController.getrecurrence);
|
||||||
|
module.exports = router;
|
||||||
12
backend/hotel_hacienda/src/routes/status.routes.js
Normal file
12
backend/hotel_hacienda/src/routes/status.routes.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const statusController = require('../controllers/status.controller');
|
||||||
|
|
||||||
|
|
||||||
|
router.put('/approveupdate/:id',statusController.setapprovalstatus);
|
||||||
|
router.put('/paymentupdate/:id',statusController.setpaymentstatus);
|
||||||
|
router.get('/penapppayments',statusController.pendingAppPayments);
|
||||||
|
router.get('/countdelaypay',statusController.countDelayPayments);
|
||||||
|
router.get('/totalspent',statusController.totalSpent);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
7
backend/hotel_hacienda/src/server.js
Normal file
7
backend/hotel_hacienda/src/server.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const app = require('./app.js'); // Importamos la app
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Servidor corriendo en http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
13
backend/hotel_hacienda/src/services/mailService.js
Normal file
13
backend/hotel_hacienda/src/services/mailService.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_HOST,
|
||||||
|
port: process.env.EMAIL_PORT,
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = transporter;
|
||||||
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: postgres_db
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: oposgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: haciendasanangel
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U oposgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
container_name: backend_app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
environment:
|
||||||
|
PORT: 4000
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: oposgres
|
||||||
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
DB_NAME: haciendasanangel
|
||||||
|
EMAIL_HOST: smtp-relay.brevo.com
|
||||||
|
EMAIL_PORT: 587
|
||||||
|
EMAIL_USER: ${EMAIL_USER}
|
||||||
|
EMAIL_PASS: ${EMAIL_PASS}
|
||||||
|
URL_CORS: https://hotel.consultoria-as.com
|
||||||
|
BANXICO_TOKEN: ${BANXICO_TOKEN}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
container_name: frontend_app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "5172:5172"
|
||||||
|
environment:
|
||||||
|
VITE_API_BASE_URL: http://backend:4000/api
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
35
frontend/Dockerfile
Normal file
35
frontend/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar archivos de dependencias
|
||||||
|
COPY Frontend-Hotel/package*.json ./
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copiar codigo fuente
|
||||||
|
COPY Frontend-Hotel/ ./
|
||||||
|
|
||||||
|
# Construir la aplicacion
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Etapa de produccion
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar archivos de dependencias
|
||||||
|
COPY Frontend-Hotel/package*.json ./
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copiar codigo fuente
|
||||||
|
COPY Frontend-Hotel/ ./
|
||||||
|
|
||||||
|
# Exponer puerto
|
||||||
|
EXPOSE 5172
|
||||||
|
|
||||||
|
# Comando de inicio (modo desarrollo para hot reload)
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
1
frontend/Frontend-Hotel/.env.example
Normal file
1
frontend/Frontend-Hotel/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:4000/api
|
||||||
44
frontend/Frontend-Hotel/.github/copilot-instructions.md
vendored
Normal file
44
frontend/Frontend-Hotel/.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Copilot Instructions for AI Coding Agents
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
- This is a React (Vite) frontend for a hotel management system.
|
||||||
|
- Main app entry: `src/main.jsx`, root component: `src/App.jsx`.
|
||||||
|
- Pages are in `src/pages/` (e.g., `Dashboard.jsx`, `LoginPage.jsx`, `Employees.jsx`).
|
||||||
|
- Reusable UI components are in `src/components/` (e.g., `Navbar/`, `Sidebar.jsx`, `Modals/`, `Table/`).
|
||||||
|
- Service modules for API calls are in `src/services/` (e.g., `api.js`, `employeeService.js`).
|
||||||
|
- Styles are in `src/styles/` and component subfolders.
|
||||||
|
|
||||||
|
## Architecture & Patterns
|
||||||
|
- **Routing**: Managed in `src/routes/ProtectedRoute.jsx` and page components.
|
||||||
|
- **State**: Local state via React hooks; no global state manager detected.
|
||||||
|
- **API**: All backend communication is abstracted in `src/services/`.
|
||||||
|
- **Component Structure**: Prefer functional components and hooks. Group related files (JSX, CSS) in subfolders.
|
||||||
|
- **UI**: Uses Tailwind CSS (see `tailwind.config.cjs`), with custom styles in `src/styles/` and per-component CSS files.
|
||||||
|
|
||||||
|
## Developer Workflows
|
||||||
|
- **Start dev server**: `npm run dev` (Vite)
|
||||||
|
- **Build for production**: `npm run build`
|
||||||
|
- **Preview production build**: `npm run preview`
|
||||||
|
- **Install dependencies**: `npm install`
|
||||||
|
- **Lint**: `npm run lint` (uses `eslint.config.js`)
|
||||||
|
- **No test scripts or test files detected**
|
||||||
|
|
||||||
|
## Conventions & Tips
|
||||||
|
- **Component Naming**: PascalCase for components, camelCase for files/variables.
|
||||||
|
- **Pages vs Components**: Pages in `src/pages/`, reusable elements in `src/components/`.
|
||||||
|
- **Service Layer**: All API logic should go in `src/services/`, not in components.
|
||||||
|
- **Assets**: Place images/icons in `public/` or `src/assets/`.
|
||||||
|
- **Modals, Tables, Filters**: Use the subfolders in `src/components/` as templates for new UI patterns.
|
||||||
|
- **Protected Routes**: Use `src/routes/ProtectedRoute.jsx` for auth-guarded navigation.
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
- **API Base URL**: Likely configured via `.env` (not shown here).
|
||||||
|
- **External Libraries**: React, Vite, Tailwind CSS, ESLint.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
- To add a new page: create a file in `src/pages/`, add a route, and (if needed) update navigation in `Sidebar.jsx` or `Navbar.jsx`.
|
||||||
|
- To add a new API call: add a function in the relevant `src/services/*.js` file and import it in your component/page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For questions about unclear patterns or missing documentation, ask the user for clarification or examples from their workflow.
|
||||||
26
frontend/Frontend-Hotel/.gitignore
vendored
Normal file
26
frontend/Frontend-Hotel/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.env
|
||||||
|
.env.git
|
||||||
29
frontend/Frontend-Hotel/eslint.config.js
Normal file
29
frontend/Frontend-Hotel/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
14
frontend/Frontend-Hotel/index.html
Normal file
14
frontend/Frontend-Hotel/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icono-svg.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hacienda San Angel</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5994
frontend/Frontend-Hotel/package-lock.json
generated
Normal file
5994
frontend/Frontend-Hotel/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
frontend/Frontend-Hotel/package.json
Normal file
44
frontend/Frontend-Hotel/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend-hotel-js",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-hook-form": "^7.66.1",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-router-dom": "^7.8.2",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
|
"yup": "^1.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.33.0",
|
||||||
|
"@types/react": "^19.1.10",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@vitejs/plugin-react-swc": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.34.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
|
"vite": "^7.1.2",
|
||||||
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3751
frontend/Frontend-Hotel/pnpm-lock.yaml
generated
Normal file
3751
frontend/Frontend-Hotel/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
frontend/Frontend-Hotel/postcss-config.js
Normal file
6
frontend/Frontend-Hotel/postcss-config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
frontend/Frontend-Hotel/public/IconoHotel.svg
Normal file
1
frontend/Frontend-Hotel/public/IconoHotel.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
1
frontend/Frontend-Hotel/public/icono-svg.svg
Normal file
1
frontend/Frontend-Hotel/public/icono-svg.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 91 KiB |
BIN
frontend/Frontend-Hotel/public/logoHotel.png
Normal file
BIN
frontend/Frontend-Hotel/public/logoHotel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/Frontend-Hotel/public/logoHotel2.png
Normal file
BIN
frontend/Frontend-Hotel/public/logoHotel2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
1
frontend/Frontend-Hotel/public/vite.svg
Normal file
1
frontend/Frontend-Hotel/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
42
frontend/Frontend-Hotel/src/App.css
Normal file
42
frontend/Frontend-Hotel/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
142
frontend/Frontend-Hotel/src/App.jsx
Normal file
142
frontend/Frontend-Hotel/src/App.jsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { BrowserRouter as Router,Routes, Route } from "react-router-dom";
|
||||||
|
import Layout from "./components/Layout2.jsx";
|
||||||
|
import Login from "./pages/Login.jsx";
|
||||||
|
import LoginAssets from "./assets/pages/login.jsx";
|
||||||
|
import BasePositiva from "./pages/BasePositiva.jsx";
|
||||||
|
import Sifen from "./pages/Sifen.jsx";
|
||||||
|
import NotFound from "./pages/NotFound.jsx";
|
||||||
|
import RoomManagement from "./pages/Settings/RoomsManagement.jsx";
|
||||||
|
//Submenú de Dashboard
|
||||||
|
import Income from "./pages/Dashboard/Income.jsx";
|
||||||
|
import Employees from "./pages/Employees.jsx";
|
||||||
|
import Contracts from "./pages/Contracts.jsx";
|
||||||
|
import HotelPL from "./pages/Dashboard/HotelPL.jsx";
|
||||||
|
import RestaurantPL from "./pages/Dashboard/RestaurantPL.jsx";
|
||||||
|
import RestaurantAnalysis from "./pages/Dashboard/RestaurantAnalysis.jsx";
|
||||||
|
import Budget from "./pages/Dashboard/Budget.jsx";
|
||||||
|
import CostPerRoom from "./pages/Dashboard/CostPerRoom.jsx";
|
||||||
|
import Expenses from "./pages/Dashboard/Expenses.jsx";
|
||||||
|
import RoomAnalysis from "./pages/Dashboard/RoomAnalysis.jsx";
|
||||||
|
//Submenú de Expenses to be approved
|
||||||
|
import PendingApproval from "./pages/PendingApproval.jsx";
|
||||||
|
import Approved from "./pages/ExpensesToBeApproval/Approved.jsx";
|
||||||
|
import Rejected from "./pages/ExpensesToBeApproval/Rejected.jsx";
|
||||||
|
//Submenú de Expenses
|
||||||
|
import ReportExpense from "./pages/Expenses/ReportExpense.jsx";
|
||||||
|
import Payments from "./pages/Expenses/Payments.jsx";
|
||||||
|
import MonthlyPayments from "./pages/Expenses/MonthlyPayments.jsx";
|
||||||
|
import MonthlyReport from "./pages/Expenses/MonthlyReport.jsx";
|
||||||
|
import ExpenseDetail from "./pages/Expenses/ExpenseDetail.jsx";
|
||||||
|
import EditExpense from "./pages/Expenses/EditExpense.jsx";
|
||||||
|
import NewExpense from "./pages/Expenses/NewExpense.jsx";
|
||||||
|
import PurchaseEntries from "./pages/Expenses/PurchaseEntries.jsx";
|
||||||
|
import NewSuppliers from "./pages/Expenses/NewSuppliers.jsx";
|
||||||
|
//Submenú de Inventory
|
||||||
|
import Products from "./pages/Inventory/Products.jsx";
|
||||||
|
import NewProduct from "./pages/Inventory/NewProduct.jsx";
|
||||||
|
import InventoryReport from "./pages/Inventory/InventoryReport.jsx";
|
||||||
|
import NewMonthlyPayment from "./pages/Expenses/NewMonthlyPayment.jsx";
|
||||||
|
import Outcomes from "./pages/Inventory/Outcomes.jsx";
|
||||||
|
import HousekeeperOutcomes from "./pages/Inventory/HousekeeperOutcomes.jsx";
|
||||||
|
|
||||||
|
import "./styles/global.css";
|
||||||
|
//Submenú de Hotel
|
||||||
|
import Properties from "./pages/Hotel/Properties.jsx";
|
||||||
|
import PropertyDetailPage from "./pages/Hotel/PropertiesId.jsx";
|
||||||
|
|
||||||
|
//Submenú de Payroll
|
||||||
|
import Payroll from "./pages/Payroll/Payroll.jsx";
|
||||||
|
import Settings from "./pages/Settings/Settings.jsx";
|
||||||
|
import SettingsId from "./pages/Settings/SettingsId.jsx";
|
||||||
|
import Plantillapayroll from "./pages/Payroll/Plantillapayroll.jsx";
|
||||||
|
import ContractsDetail from "./pages/Payroll/ContractsDetail.jsx";
|
||||||
|
import PayrollContract from "./pages/Payroll/PayrollContract.jsx";
|
||||||
|
import EditPayroll from "./pages/Payroll/EditPayroll.jsx";
|
||||||
|
|
||||||
|
//Submenú de Inventory
|
||||||
|
|
||||||
|
import Adjustments from "./pages/Inventory/Adjustments.jsx";
|
||||||
|
import AlterProduct from "./pages/Inventory/AlterProduct.jsx";
|
||||||
|
import PayrollAttendance from "./pages/Payroll/PayrollAttendance.jsx";
|
||||||
|
import PayrollEmployees from "./pages/Payroll/PayrollEmployees.jsx";
|
||||||
|
import PayrollNewEmployee from "./pages/Payroll/NewEmployee.jsx";
|
||||||
|
|
||||||
|
import NewIncome from "./pages/Income/NewIncome.jsx";
|
||||||
|
import IncomeReport from "./pages/Income/IncomeReport.jsx";
|
||||||
|
import DiscardProduct from "./pages/Inventory/DiscardProduct.jsx";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
// const location = useLocation();
|
||||||
|
// const isLogin = location.pathname === "/";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <Navbar /> */}
|
||||||
|
{/**{!isLogin && <Navbar />} */}
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Login />} />
|
||||||
|
<Route path="/app" element={<Layout />}>
|
||||||
|
<Route path="login" element={<LoginAssets />} />
|
||||||
|
<Route path="base-positiva" element={<BasePositiva />} />
|
||||||
|
<Route path="sifen" element={<Sifen />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
<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="restaurant-analysis" element={<RestaurantAnalysis />} />
|
||||||
|
<Route path="budget" element={<Budget />} />
|
||||||
|
<Route path="cost-per-room" element={<CostPerRoom />} />
|
||||||
|
<Route path="room-analysis" element={<RoomAnalysis />} />
|
||||||
|
<Route path="expenses" element={<Expenses />} />
|
||||||
|
<Route path="new-income-report" element={<IncomeReport />} />
|
||||||
|
<Route path="new-income-form" element={<NewIncome />} />
|
||||||
|
<Route path="edit-income-form/:id" element={<NewIncome />} />
|
||||||
|
<Route path="payments" element={<Payments />} />
|
||||||
|
<Route path="pending-approval" element={<PendingApproval />} />
|
||||||
|
<Route path="approved" element={<Approved />} />
|
||||||
|
<Route path="rejected" element={<Rejected />} />
|
||||||
|
<Route path="report-expense" element={<ReportExpense />} />
|
||||||
|
<Route path="new-expense" element={<NewExpense />} />
|
||||||
|
<Route path="monthly-payments" element={<MonthlyPayments />} />
|
||||||
|
<Route path="expenses/edit/:id" element={<EditExpense />} />
|
||||||
|
<Route path="expenses/:id" element={<ExpenseDetail />} />
|
||||||
|
<Route path="purchase-entries" element={<PurchaseEntries />} />
|
||||||
|
<Route path="products" element={<Products />} />
|
||||||
|
<Route path="properties" element={<Properties />} />
|
||||||
|
<Route path="payroll" element={<Payroll />} />
|
||||||
|
<Route path="properties/:id" element={<PropertyDetailPage />} />
|
||||||
|
<Route path="new-product" element={<NewProduct/>} />
|
||||||
|
<Route path="inventory-report" element={<InventoryReport />} />
|
||||||
|
<Route path="settings" element={<Settings/>} />
|
||||||
|
<Route path="settings/room-management" element={<RoomManagement />} />
|
||||||
|
<Route path="settings/settings-id" element={<SettingsId />} />
|
||||||
|
<Route path="new-monthly" element={<NewMonthlyPayment/>} />
|
||||||
|
<Route path="new-suppliers" element={<NewSuppliers/>} />
|
||||||
|
<Route path="monthly-report" element={<MonthlyReport />} />
|
||||||
|
|
||||||
|
<Route path="discard-product" element={<DiscardProduct />} />
|
||||||
|
<Route path="adjustments" element={<Adjustments />} />
|
||||||
|
<Route path="product-adjustments" element={<Adjustments />} />
|
||||||
|
<Route path="payroll/employees" element={<PayrollEmployees />} />
|
||||||
|
<Route path="payroll/attendance" element={<PayrollAttendance />} />
|
||||||
|
<Route path="payroll/newemployee" element={<PayrollNewEmployee />} />
|
||||||
|
<Route path="payroll/employee/:id" element={<PayrollNewEmployee />} />
|
||||||
|
<Route path="payroll/contract" element={<PayrollContract />} />
|
||||||
|
<Route path="payroll/contract/:id" element={<PayrollContract />} />
|
||||||
|
<Route path="payroll/edit/:id" element={<EditPayroll />} />
|
||||||
|
<Route path="payroll/:id" element={<Plantillapayroll />} />
|
||||||
|
<Route path="payroll/contracts-detail/:id" element={<ContractsDetail />} />
|
||||||
|
<Route path="alter-product/:id" element={<AlterProduct />} />
|
||||||
|
<Route path="inventory/outcomes" element={<Outcomes />} />
|
||||||
|
<Route path="housekeeper/outcomes" element={<HousekeeperOutcomes />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <Route path="users" element={<Users/>} />
|
||||||
|
<Route path="reportes" element={<Reportes />} />
|
||||||
|
*/
|
||||||
63
frontend/Frontend-Hotel/src/assets/pages/login.jsx
Normal file
63
frontend/Frontend-Hotel/src/assets/pages/login.jsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import "../../components/Buttons/Button";
|
||||||
|
function LoginAssets() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
alert(`Email: ${email}, Password: ${password}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-[#e0b400]">
|
||||||
|
<div className="bg-white shadow-lg rounded-lg flex p-10 max-w-3xl w-full">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex justify-center items-center w-1/2">
|
||||||
|
<img
|
||||||
|
src="/logo.png" // 👈 coloca aquí tu logo (ponlo en public/logo.png)
|
||||||
|
alt="Logo"
|
||||||
|
className="max-h-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulario */}
|
||||||
|
<div className="flex flex-col justify-center w-1/2">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<button className="gray">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="EMAIL/USER"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 rounded-full bg-gray-100 focus:outline-none focus:ring-2 focus:ring-[#720019]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="PASSWORD"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-[#720019] text-white font-bold px-6 py-2 rounded-full hover:bg-[#5a0014] transition"
|
||||||
|
>
|
||||||
|
LOG IN
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-[#720019] text-white font-bold px-6 py-2 rounded-full hover:bg-[#5a0014] transition"
|
||||||
|
>
|
||||||
|
RECOVER PASSWORD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginAssets;
|
||||||
1
frontend/Frontend-Hotel/src/assets/react.svg
Normal file
1
frontend/Frontend-Hotel/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
89
frontend/Frontend-Hotel/src/components/Buttons/Button.css
Normal file
89
frontend/Frontend-Hotel/src/components/Buttons/Button.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 999px; /* Súper redondo */
|
||||||
|
background-color: #521414;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ffffff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background-color: #4a0d0d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0px 12px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secundary {
|
||||||
|
border: none;
|
||||||
|
padding: 20px 40px;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #555555;
|
||||||
|
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secundary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0px 12px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
color: #555;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 20px 40px;
|
||||||
|
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.gray {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
*/
|
||||||
18
frontend/Frontend-Hotel/src/components/Buttons/Button.jsx
Normal file
18
frontend/Frontend-Hotel/src/components/Buttons/Button.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// src/components/Button.jsx
|
||||||
|
import React from "react";
|
||||||
|
import "./Button.css"; // estilos separados
|
||||||
|
|
||||||
|
function Button({ label, onClick, variant = "primary" | "secundary"}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Login</h1>
|
||||||
|
<Button variant="primary" onClick={() => alert("Ingresando...")}>
|
||||||
|
Iniciar sesión
|
||||||
|
</Button>
|
||||||
|
<Button variant="secundary" onClick={() => alert("Registro")}>
|
||||||
|
Registrarse
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default Button;
|
||||||
118
frontend/Frontend-Hotel/src/components/ExcelExportButton.jsx
Normal file
118
frontend/Frontend-Hotel/src/components/ExcelExportButton.jsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import { FiDownload } from "react-icons/fi";
|
||||||
|
import { langContext } from "../context/LenguageContext";
|
||||||
|
|
||||||
|
export default function ExcelExportButton({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
filenamePrefix,
|
||||||
|
sheetName,
|
||||||
|
dataTransform = null,
|
||||||
|
className = "",
|
||||||
|
style = {},
|
||||||
|
}) {
|
||||||
|
const { lang } = useContext(langContext);
|
||||||
|
|
||||||
|
const handleExportToExcel = () => {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
alert(lang === "es" ? "No hay datos para exportar" : "No data to export");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataToExport = dataTransform ? dataTransform(data) : data;
|
||||||
|
|
||||||
|
const excelData = dataToExport.map((row) => {
|
||||||
|
const excelRow = {};
|
||||||
|
columns.forEach((column) => {
|
||||||
|
if (column.key) {
|
||||||
|
let value = row[column.key];
|
||||||
|
|
||||||
|
// Handle render functions (extract text value)
|
||||||
|
if (column.render && typeof column.render === "function") {
|
||||||
|
// Try to get the raw value if render is used
|
||||||
|
value = row[column.key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
value = isNaN(value.getTime())
|
||||||
|
? ""
|
||||||
|
: value.toLocaleDateString(lang === "es" ? "es-MX" : "en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
const header =
|
||||||
|
typeof column.header === "string"
|
||||||
|
? column.header
|
||||||
|
: (lang === "es" ? column.header?.es : column.header?.en) ||
|
||||||
|
column.key;
|
||||||
|
|
||||||
|
excelRow[header] = value || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return excelRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
const finalSheetName =
|
||||||
|
typeof sheetName === "string"
|
||||||
|
? sheetName
|
||||||
|
: (lang === "es" ? sheetName?.es : sheetName?.en) || "Sheet1";
|
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, finalSheetName);
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const dateStr = today.toISOString().split("T")[0];
|
||||||
|
const filename = `${filenamePrefix}-${dateStr}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(workbook, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultStyle = {
|
||||||
|
backgroundColor: "#ffcb05",
|
||||||
|
color: "#fff",
|
||||||
|
padding: "10px 20px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "8px",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
fontSize: "14px",
|
||||||
|
transition: "background-color 0.3s ease",
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleExportToExcel}
|
||||||
|
className={className}
|
||||||
|
style={defaultStyle}
|
||||||
|
onMouseEnter={(e) => (e.target.style.backgroundColor = "#f4b400")}
|
||||||
|
onMouseLeave={(e) => (e.target.style.backgroundColor = "#ffcb05")}
|
||||||
|
>
|
||||||
|
<FiDownload />
|
||||||
|
{lang === "es" ? "Exportar a Excel" : "Export to Excel"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExcelExportButton.propTypes = {
|
||||||
|
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
header: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
render: PropTypes.func,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
filenamePrefix: PropTypes.string.isRequired,
|
||||||
|
sheetName: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
|
||||||
|
.isRequired,
|
||||||
|
dataTransform: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.date-range-filter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 0 2px #f4f4f4;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
min-width: 150px;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
height: 42px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px #fcd200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input:hover {
|
||||||
|
box-shadow: 0 0 0 2px #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.date-range-filter {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './DateRangeFilter.css';
|
||||||
|
|
||||||
|
const DateRangeFilter = ({
|
||||||
|
dateRange,
|
||||||
|
onDateChange,
|
||||||
|
labels = { from: 'From:', to: 'To:' },
|
||||||
|
lang = 'en'
|
||||||
|
}) => {
|
||||||
|
const fromLabel = lang === 'en' ? labels.from : 'Desde:';
|
||||||
|
const toLabel = lang === 'en' ? labels.to : 'Hasta:';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="date-range-filter">
|
||||||
|
<div className="date-input-group">
|
||||||
|
<label htmlFor="date-from" className="date-label">
|
||||||
|
{fromLabel}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="date-from"
|
||||||
|
type="date"
|
||||||
|
className="date-input"
|
||||||
|
value={dateRange.from}
|
||||||
|
onChange={(e) => onDateChange({ ...dateRange, from: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="date-input-group">
|
||||||
|
<label htmlFor="date-to" className="date-label">
|
||||||
|
{toLabel}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="date-to"
|
||||||
|
type="date"
|
||||||
|
className="date-input"
|
||||||
|
value={dateRange.to}
|
||||||
|
onChange={(e) => onDateChange({ ...dateRange, to: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateRangeFilter;
|
||||||
|
|
||||||
265
frontend/Frontend-Hotel/src/components/Filters/Filters.css
Normal file
265
frontend/Frontend-Hotel/src/components/Filters/Filters.css
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/* src/styles/Filters.css */
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-payment-btn {
|
||||||
|
background-color: #FFD700;
|
||||||
|
color: #800000;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-payment-btn:hover {
|
||||||
|
background-color: #fcd200;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-payment-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-page {
|
||||||
|
padding: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
input[type="date"] {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 0 2px #f4f4f4;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
max-width: 250px;
|
||||||
|
min-width: 150px;
|
||||||
|
width: auto;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
/* appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none; */
|
||||||
|
/* background-image: url("data:image/svg+xml,%3Csvg fill='gold' viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 2px center;
|
||||||
|
background-size: 18px 18px; */
|
||||||
|
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px; */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus,
|
||||||
|
input[type="date"]:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px #fcd200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select, .filter-date {
|
||||||
|
background-color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 0 3px rgba(0,0,0,0.1);
|
||||||
|
appearance: none;
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg fill="gold" ...>...</svg>'); /* usa ícono amarillo aquí */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
background-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:focus, .filter-date:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button {
|
||||||
|
border: 2px solid;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button.approve {
|
||||||
|
color: #28a745;
|
||||||
|
border-color: #28a745;
|
||||||
|
background-color: #e6fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button.approved {
|
||||||
|
color: #28a745;
|
||||||
|
border-color: #28a745;
|
||||||
|
background-color: #e6fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button.reject {
|
||||||
|
color: #dc3545;
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
}
|
||||||
|
.status-button.rejected {
|
||||||
|
color: #dc3545;
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button.pending {
|
||||||
|
color: #f0ad4e;
|
||||||
|
border-color: #f0ad4e;
|
||||||
|
background-color: #fff7e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button.paid {
|
||||||
|
color: #28a745;
|
||||||
|
border-color: #28a745;
|
||||||
|
background-color: #e6fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.income-card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
min-width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .summary-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 160px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
min-width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
align-items: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Date*/
|
||||||
|
.date-filter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-left: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.status-button.approve {
|
||||||
|
background-color: #8bed92;
|
||||||
|
border: 1.5px solid #33a544;
|
||||||
|
color: #33a544;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 5px 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
pointer-events: none;
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
.page-filters {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approved-page h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
.status-button {
|
||||||
|
border: 2px solid #28a745;
|
||||||
|
color: #28a745;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #e6fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-button.inactive {
|
||||||
|
border-color: #dc3545;
|
||||||
|
color: #dc3545;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
}
|
||||||
|
*/
|
||||||
99
frontend/Frontend-Hotel/src/components/Filters/Filters.jsx
Normal file
99
frontend/Frontend-Hotel/src/components/Filters/Filters.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// src/components/Filters/Filters.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import './Filters.css';
|
||||||
|
|
||||||
|
const Filters = ({
|
||||||
|
areaOptions = [],
|
||||||
|
statusOptions = [],
|
||||||
|
selectedArea,
|
||||||
|
selectedStatus,
|
||||||
|
onAreaChange,
|
||||||
|
onStatusChange,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
onStartDateChange,
|
||||||
|
onEndDateChange,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="filters-container">
|
||||||
|
{areaOptions.length > 0 && (
|
||||||
|
<div className="filter-select-wrapper">
|
||||||
|
<select className="filter-select" value={selectedArea} onChange={onAreaChange}>
|
||||||
|
{areaOptions.map((area, index) => (
|
||||||
|
<option key={index} value={area}>
|
||||||
|
{area}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{statusOptions.length > 0 && (
|
||||||
|
<div className="filter-select-wrapper">
|
||||||
|
<select className="filter-select" value={selectedStatus} onChange={onStatusChange}>
|
||||||
|
{statusOptions.map((status, index) => (
|
||||||
|
<option key={index} value={status}>
|
||||||
|
{status}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onStartDateChange && (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="filter-date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={onStartDateChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onEndDateChange && (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="filter-date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={onEndDateChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Filters;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// src/components/Filters.jsx
|
||||||
|
// import React from 'react';
|
||||||
|
// import '../styles/Filters.css';
|
||||||
|
|
||||||
|
// export default function Filters({ area, status, startDate, endDate, onChange }) {
|
||||||
|
// return (
|
||||||
|
// <div className="filters">
|
||||||
|
// <select value={area} onChange={(e) => onChange('area', e.target.value)}>
|
||||||
|
// <option value="">Area: Hotel, Restaurant</option>
|
||||||
|
// <option value="Hotel">Hotel</option>
|
||||||
|
// <option value="Restaurant">Restaurant</option>
|
||||||
|
// </select>
|
||||||
|
|
||||||
|
// <select value={status} onChange={(e) => onChange('status', e.target.value)}>
|
||||||
|
// <option value="">Status: Active, Inactive</option>
|
||||||
|
// <option value="Active">Active</option>
|
||||||
|
// <option value="Inactive">Inactive</option>
|
||||||
|
// </select>
|
||||||
|
|
||||||
|
// <input
|
||||||
|
// type="date"
|
||||||
|
// value={startDate}
|
||||||
|
// onChange={(e) => onChange('startDate', e.target.value)}
|
||||||
|
// />
|
||||||
|
// <input
|
||||||
|
// type="date"
|
||||||
|
// value={endDate}
|
||||||
|
// onChange={(e) => onChange('endDate', e.target.value)}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
32
frontend/Frontend-Hotel/src/components/FormInput.jsx
Normal file
32
frontend/Frontend-Hotel/src/components/FormInput.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// export default function FormInput({ label, name, value, onChange, ...rest }) {
|
||||||
|
// return (
|
||||||
|
// <div className="form-input">
|
||||||
|
// {label && <label>{label}</label>}
|
||||||
|
// <input
|
||||||
|
// name={name}
|
||||||
|
// value={value}
|
||||||
|
// onChange={onChange}
|
||||||
|
// {...rest}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function FormInput({ label, name, value, onChange, placeholder, type = "text", ...props }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>{label}</label>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
frontend/Frontend-Hotel/src/components/FormSelect.jsx
Normal file
13
frontend/Frontend-Hotel/src/components/FormSelect.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default function FormSelect({ label, name, value, onChange, options }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>{label}</label>
|
||||||
|
<select name={name} value={value} onChange={onChange}>
|
||||||
|
<option value="">Select</option>
|
||||||
|
{options.map(opt => (
|
||||||
|
<option key={opt} value={opt}>{opt}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
frontend/Frontend-Hotel/src/components/Inputs/Input.css
Normal file
8
frontend/Frontend-Hotel/src/components/Inputs/Input.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
17
frontend/Frontend-Hotel/src/components/Inputs/Input.jsx
Normal file
17
frontend/Frontend-Hotel/src/components/Inputs/Input.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// src/components/Input.jsx
|
||||||
|
import React from "react";
|
||||||
|
import "./Input.css";
|
||||||
|
|
||||||
|
function Input({ type = "text", placeholder, value, onChange }) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input;
|
||||||
357
frontend/Frontend-Hotel/src/components/Layout.jsx
Normal file
357
frontend/Frontend-Hotel/src/components/Layout.jsx
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
// Layout.jsx
|
||||||
|
// import React, { useState } from "react";
|
||||||
|
// import { Outlet, NavLink, useLocation } from "react-router-dom";
|
||||||
|
// import { useNavigate } from 'react-router-dom';
|
||||||
|
// import { FaBell, FaCog } from "react-icons/fa";
|
||||||
|
// import "../styles/Dashboard.css";
|
||||||
|
|
||||||
|
// const menuConfig = [
|
||||||
|
// {
|
||||||
|
// label: "Dashboards",
|
||||||
|
// basePath: "/app/income",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Income", route: "/app/income" },
|
||||||
|
// { label: "Expenses", route: "/app/expenses" },
|
||||||
|
// { label: "Cost per room", route: "/app/cost-per-room" },
|
||||||
|
// { label: "Budget", route: "/app/budget" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Expenses to be approved",
|
||||||
|
// basePath: "/app/pending-approval",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Pending approval", route: "/app/pending-approval" },
|
||||||
|
// { label: "Approved", route: "/app/approved" },
|
||||||
|
// { label: "Rejected", route: "/app/rejected" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Expenses",
|
||||||
|
// basePath: "/app/report",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Report", route: "/app/report" },
|
||||||
|
// { label: "New Expense", route: "/app/new-expense" },
|
||||||
|
// { label: "Payments", route: "/app/payments" },
|
||||||
|
// { label: "Monthly Payments", route: "/app/monthly-payments" },
|
||||||
|
// { label: "Id", route: "/app/expense-id" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Inventory",
|
||||||
|
// basePath: "/app/products",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Products", route: "/app/products" },
|
||||||
|
// { label: "New Product", route: "/app/new-product" },
|
||||||
|
// { label: "Report", route: "/app/inventory-report" },
|
||||||
|
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Payroll",
|
||||||
|
// basePath: "/app/payroll",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Report", route: "/app/payroll" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Hotel",
|
||||||
|
// basePath: "/app/properties",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Properties", route: "/app/properties" },
|
||||||
|
// // { label: "New Property", route: "/app/properties/:id"},
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// export default function Layout() {
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
// const location = useLocation();
|
||||||
|
// const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
// const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
||||||
|
// const isDetailPage = /^\/app\/properties\/\d+$/.test(location.pathname);
|
||||||
|
// const isSettingsPage = location.pathname === "/app/settings";
|
||||||
|
// // Detectar qué menú está activo según la ruta actual
|
||||||
|
// // const activeSection = menuConfig.find(section =>
|
||||||
|
// // location.pathname.startsWith(section.basePath)
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // const activeSection = menuConfig.find(section =>
|
||||||
|
// // section.submenu.some(item => location.pathname.startsWith(item.route))
|
||||||
|
// // );
|
||||||
|
|
||||||
|
// // Encuentra la sección activa o ignórala si es una ruta especial como "/app/properties/:id"
|
||||||
|
// const activeSection = menuConfig.find(section =>
|
||||||
|
// section.submenu.some(item => location.pathname.startsWith(item.route))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Si no hay sección activa, es una página sin menú (como detalles)
|
||||||
|
// const activeSubmenu = activeSection?.submenu || [];
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="dashboard-layout">
|
||||||
|
// {/* Sidebar */}
|
||||||
|
// {isSidebarOpen && (
|
||||||
|
// <aside className="sidebar">
|
||||||
|
// <nav>
|
||||||
|
// <NavLink to="/app/income">Dashboards</NavLink>
|
||||||
|
// <NavLink to="/app/pending-approval">Expenses to be approved</NavLink>
|
||||||
|
// <NavLink to="/app/report">Expenses</NavLink>
|
||||||
|
// <NavLink to="/app/products">Inventory</NavLink>
|
||||||
|
// <NavLink to="/app/payroll">Payroll</NavLink>
|
||||||
|
// <NavLink to="/app/properties">Hotel</NavLink>
|
||||||
|
// </nav>
|
||||||
|
// </aside>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Main content */}
|
||||||
|
// <div className="main-content">
|
||||||
|
// {/* Topbar */}
|
||||||
|
// <div className="topbar">
|
||||||
|
// <div className="topbar-header">
|
||||||
|
// {/* Oculta título si estamos en /app/settings */}
|
||||||
|
// {!isSettingsPage && (
|
||||||
|
// <div className="topbar-title">{activeSection?.label}</div>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// <div className="topbar-icons">
|
||||||
|
// <FaBell className="topbar-icon" />
|
||||||
|
// <FaCog
|
||||||
|
// className="topbar-icon cursor-pointer"
|
||||||
|
// onClick={() => navigate("/app/settings")}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/*Oculta submenú si es página de detalles o settings */}
|
||||||
|
// {!isDetailPage && !isSettingsPage && (
|
||||||
|
// <div className="topbar-submenu">
|
||||||
|
// {activeSubmenu.map((item, index) => (
|
||||||
|
// <NavLink
|
||||||
|
// key={index}
|
||||||
|
// to={item.route}
|
||||||
|
// className={({ isActive }) =>
|
||||||
|
// isActive ? "submenu-link active" : "submenu-link"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {item.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Página actual */}
|
||||||
|
// <div className="content">
|
||||||
|
// <Outlet />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="dashboard-layout">
|
||||||
|
// {/* Sidebar */}
|
||||||
|
// {isSidebarOpen && (
|
||||||
|
// <aside className="sidebar">
|
||||||
|
// <nav>
|
||||||
|
// <NavLink to="/app/income">Dashboards</NavLink>
|
||||||
|
// <NavLink to="/app/pending-approval">Expenses to be approved</NavLink>
|
||||||
|
// <NavLink to="/app/report">Expenses</NavLink>
|
||||||
|
// <NavLink to="/app/products">Inventory</NavLink>
|
||||||
|
// <NavLink to="/app/payroll">Payroll</NavLink>
|
||||||
|
// <NavLink to="/app/properties">Hotel</NavLink>
|
||||||
|
// </nav>
|
||||||
|
// </aside>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Contenido principal */}
|
||||||
|
// <div className="main-content">
|
||||||
|
// {/* ÚNICO Topbar */}
|
||||||
|
// <div className="topbar">
|
||||||
|
// {/* Línea superior: título + iconos */}
|
||||||
|
// <div className="topbar-header">
|
||||||
|
// <div className="topbar-title">{activeSection?.label}</div>
|
||||||
|
// <div className="topbar-icons">
|
||||||
|
// <FaBell className="topbar-icon" />
|
||||||
|
// <FaCog className="topbar-icon cursor-pointer" onClick={() => navigate("/app/settings")} />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Línea inferior: submenú dinámico */}
|
||||||
|
|
||||||
|
// {/* Línea inferior: submenú dinámico */}
|
||||||
|
// {!isDetailPage && (
|
||||||
|
// <div className="topbar-submenu">
|
||||||
|
// {activeSubmenu.map((item, index) => (
|
||||||
|
// <NavLink
|
||||||
|
// key={index}
|
||||||
|
// to={item.route}
|
||||||
|
// className={({ isActive }) =>
|
||||||
|
// isActive ? "submenu-link active" : "submenu-link"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {item.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* <div className="topbar-submenu">
|
||||||
|
// {activeSubmenu.map((item, index) => (
|
||||||
|
// <NavLink
|
||||||
|
// key={index}
|
||||||
|
// to={item.route}
|
||||||
|
// className={({ isActive }) =>
|
||||||
|
// isActive ? "submenu-link active" : "submenu-link"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {item.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </div> */}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Aquí va el contenido de la página */}
|
||||||
|
// <div className="content">
|
||||||
|
// <Outlet />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//{ label: "Property", route: "/app/properties/:id" },
|
||||||
|
|
||||||
|
// import React from "react";
|
||||||
|
// import { Outlet, useLocation, NavLink } from "react-router-dom";
|
||||||
|
// import { menuConfig } from "../constants/menuConfig";
|
||||||
|
// import { FaBell, FaCog } from "react-icons/fa";
|
||||||
|
// import "../styles/Dashboard.css";
|
||||||
|
|
||||||
|
// export default function Layout() {
|
||||||
|
// const location = useLocation();
|
||||||
|
// const pathname = location.pathname;
|
||||||
|
|
||||||
|
// // Encuentra la sección activa
|
||||||
|
// const activeSectionKey = Object.keys(menuConfig).find((key) =>
|
||||||
|
// pathname.startsWith(menuConfig[key].baseRoute)
|
||||||
|
// );
|
||||||
|
// const activeSection = menuConfig[activeSectionKey];
|
||||||
|
// const activeSubmenu = activeSection?.submenu || [];
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="dashboard-layout">
|
||||||
|
// {/* SIDEBAR */}
|
||||||
|
// <aside className="sidebar">
|
||||||
|
// <nav>
|
||||||
|
// {Object.entries(menuConfig).map(([key, section]) => (
|
||||||
|
// <NavLink
|
||||||
|
// key={key}
|
||||||
|
// to={section.baseRoute}
|
||||||
|
// className={({ isActive }) =>
|
||||||
|
// isActive ? "menu-items a active" : "menu-items a"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {section.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </nav>
|
||||||
|
// </aside>
|
||||||
|
|
||||||
|
// {/* CONTENIDO PRINCIPAL */}
|
||||||
|
// <div className="main-content">
|
||||||
|
// {/* TOPBAR */}
|
||||||
|
// <div className="topbar">
|
||||||
|
// <div className="topbar-header">
|
||||||
|
// <div className="topbar-title">{activeSection?.label}</div>
|
||||||
|
// <div className="topbar-icons">
|
||||||
|
// <FaBell className="topbar-icon" />
|
||||||
|
// <FaCog className="topbar-icon" />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="topbar-submenu">
|
||||||
|
// {activeSubmenu.map((item, index) => (
|
||||||
|
// <NavLink
|
||||||
|
// key={index}
|
||||||
|
// to={item.route}
|
||||||
|
// className={({ isActive }) =>
|
||||||
|
// isActive ? "submenu-link active" : "submenu-link"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {item.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
|
||||||
|
// {/* CONTENIDO */}
|
||||||
|
// <div className="content">
|
||||||
|
// <Outlet />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// // src/components/Layout.jsx
|
||||||
|
// import React, { useState } from "react";
|
||||||
|
// import { Outlet, NavLink } from "react-router-dom";
|
||||||
|
// import "../styles/Dashboard.css";
|
||||||
|
|
||||||
|
// export default function Layout() {
|
||||||
|
// const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
|
// const toggleSidebar = () => {
|
||||||
|
// setSidebarOpen(!isSidebarOpen);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="dashboard-layout">
|
||||||
|
|
||||||
|
// {/* Sidebar */}
|
||||||
|
// {isSidebarOpen && (
|
||||||
|
// <aside className="sidebar">
|
||||||
|
// <nav>
|
||||||
|
// <h1></h1>
|
||||||
|
// <NavLink to="/app/dashboard">Dashboards</NavLink>
|
||||||
|
// <NavLink to="/app/expenses-to-approve">Expenses to be approved</NavLink>
|
||||||
|
// <NavLink to="/app/expenses">Expenses</NavLink>
|
||||||
|
// <NavLink to="/app/inventory">Inventory</NavLink>
|
||||||
|
// <NavLink to="/app/payroll">Payroll</NavLink>
|
||||||
|
// <NavLink to="/app/hotel">Hotel</NavLink>
|
||||||
|
// <NavLink to="/app/income">Income</NavLink>
|
||||||
|
// <NavLink to="/app/employees">Employees</NavLink>
|
||||||
|
// <NavLink to="/app/contracts">Contracts</NavLink>
|
||||||
|
// <NavLink to="/app/payments">Payments</NavLink>
|
||||||
|
// <NavLink to="/app/pending-approval">PendingApproval</NavLink>
|
||||||
|
// </nav>
|
||||||
|
// </aside>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Contenedor principal */}
|
||||||
|
// <div className="main-content">
|
||||||
|
// {/* Topbar */}
|
||||||
|
// <div className="topbar">
|
||||||
|
// <button onClick={toggleSidebar} style={{ fontSize: "1.2rem", marginRight: "auto", background: "none", border: "none", color: "white", cursor: "pointer" }}>
|
||||||
|
// ☰
|
||||||
|
// </button>
|
||||||
|
// <span >Dashboard</span> {/* Cambia esto dinámicamente si deseas */}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Contenido de cada página */}
|
||||||
|
// <div className="content">
|
||||||
|
// <Outlet />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// <NavLink to="/app/users">Users</NavLink>
|
||||||
426
frontend/Frontend-Hotel/src/components/Layout2.jsx
Normal file
426
frontend/Frontend-Hotel/src/components/Layout2.jsx
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
// Layout.jsx
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { Outlet, NavLink, useLocation } from "react-router-dom";
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import "../styles/Dashboard.css";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
import { langContext } from "../context/LenguageContext";
|
||||||
|
import { FaSignOutAlt } from "react-icons/fa";
|
||||||
|
import { menuConfig } from "../constants/menuconfig";
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
const { toggleLang, lang } = useContext(langContext);
|
||||||
|
const { user, logout } = useContext(AuthContext);
|
||||||
|
console.log('user', user);
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
navigate("/");
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
submenu: section.submenu?.map(item => ({
|
||||||
|
...item,
|
||||||
|
hidden: item.hidden ||
|
||||||
|
(section.label === "Expenses" && user === 2 && item.label !== "Report" && item.label !== "Monthly Report" ? true : false) ||
|
||||||
|
(section.label === "Payroll" && user === 2 && !["Report", "Attendance", "Employees", "Contracts"].includes(item.label) ? true : false) ||
|
||||||
|
(section.label === "Expenses" && user === 5 && !["New Expense", "Purchase Entries", "New Suppliers"].includes(item.label) ? true : false)
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
||||||
|
// const isDetailPage = /^\/app\/properties\/\d+$/.test(location.pathname);
|
||||||
|
// Detectar páginas de detalle (para ocultar el submenú)
|
||||||
|
const isDetailPage =
|
||||||
|
/^\/app\/properties\/\d+$/.test(location.pathname) || // Propiedades (ya existente)
|
||||||
|
/^\/app\/payroll\/contracts-detail(\/.*)?$/.test(location.pathname) || // Contract Detail
|
||||||
|
/^\/app\/expenses\/detail(\/.*)?$/.test(location.pathname); // Otros detalles si los tienes
|
||||||
|
|
||||||
|
|
||||||
|
const activeSection = menuConfigWithPermissions.find(section => {
|
||||||
|
if (section.hidden) return false;
|
||||||
|
|
||||||
|
const matchesSubmenu = section.submenu.some(item => location.pathname.startsWith(item.route));
|
||||||
|
const matchesBasePath = location.pathname.startsWith(section.basePath);
|
||||||
|
|
||||||
|
if (matchesSubmenu || matchesBasePath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.label === "Income" && location.pathname.startsWith("/app/edit-income-form")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.label === "Expenses" && (location.pathname.startsWith("/app/expenses/edit/") || location.pathname.startsWith("/app/expenses/") || location.pathname === "/app/new-monthly")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.label === "Inventory" && location.pathname.startsWith("/app/alter-product/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.label === "Payroll" && (location.pathname.startsWith("/app/payroll/employee/") || location.pathname.startsWith("/app/payroll/contract/") || location.pathname.startsWith("/app/payroll/edit/") || location.pathname.startsWith("/app/payroll/contracts-detail/"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.label === "Hotel" && location.pathname.startsWith("/app/properties/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeSubmenu = activeSection?.label === "Housekeeper"
|
||||||
|
? [{ label: "Outcomes", spanish_label: "Salidas", route: "/app/housekeeper/outcomes" }]
|
||||||
|
: activeSection?.submenu || [];
|
||||||
|
|
||||||
|
const isLandingPage = location.pathname === '/app' || !activeSection;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard-layout">
|
||||||
|
{/* Sidebar */}
|
||||||
|
{isSidebarOpen && (
|
||||||
|
<aside className="sidebar">
|
||||||
|
<nav>
|
||||||
|
{/*sSolo se muestran secciones que no están ocultas */}
|
||||||
|
{menuConfigWithPermissions
|
||||||
|
.filter(section => !section.hidden)
|
||||||
|
.map((section, index) => (
|
||||||
|
<NavLink key={index} to={section.basePath}>
|
||||||
|
{lang === "en" ? section.label : section.spanish_label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="main-content">
|
||||||
|
{/* Topbar */}
|
||||||
|
<div className="topbar">
|
||||||
|
<div className="topbar-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
|
||||||
|
{/* Botón + Título (alineados a la izquierda) */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<button onClick={toggleSidebar} className="sidebar-toggle-button">
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="topbar-title">
|
||||||
|
{lang === "en" ? activeSection?.label : activeSection?.spanish_label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Iconos a la derecha */}
|
||||||
|
<div className="topbar-icons" style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
<select
|
||||||
|
|
||||||
|
|
||||||
|
className="language-select"
|
||||||
|
onChange={toggleLang}
|
||||||
|
>
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="es">ES</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "6px",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#ffffffff",
|
||||||
|
border: "none",
|
||||||
|
fontWeight: "bold",
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaSignOutAlt className="topbar-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submenú (solo si no es página de detalle) */}
|
||||||
|
{!isDetailPage && (
|
||||||
|
<div className="topbar-submenu">
|
||||||
|
{activeSubmenu.filter(section => !section.hidden).map((item, index) => (
|
||||||
|
<NavLink
|
||||||
|
key={index}
|
||||||
|
to={item.route}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
isActive ? "submenu-link active" : "submenu-link"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{lang === "en" ? item.label : item.spanish_label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Página actual */}
|
||||||
|
<div className="content">
|
||||||
|
{isLandingPage ? (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
gap: '2rem'
|
||||||
|
}}>
|
||||||
|
<img
|
||||||
|
src="/logoHotel.png"
|
||||||
|
alt="Hotel Logo"
|
||||||
|
style={{
|
||||||
|
maxWidth: '300px',
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
color: '#666',
|
||||||
|
textAlign: 'center',
|
||||||
|
maxWidth: '600px',
|
||||||
|
padding: '0 1rem'
|
||||||
|
}}>
|
||||||
|
{lang === "en"
|
||||||
|
? "To get started, select an option from the menu on the left."
|
||||||
|
: "Para comenzar, selecciona una opción del menú de la izquierda."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Outlet />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Layout.jsx
|
||||||
|
// import React, { use, useContext, useState } from "react";
|
||||||
|
// import { Outlet, NavLink, useLocation } from "react-router-dom";
|
||||||
|
// import { useNavigate } from 'react-router-dom';
|
||||||
|
// import { FaBell, FaCog } from "react-icons/fa";
|
||||||
|
// import "../styles/Dashboard.css";
|
||||||
|
// import { AuthContext } from "../context/AuthContext";
|
||||||
|
// import { langContext } from "../context/LenguageContext";
|
||||||
|
// import { FaSignOutAlt } from "react-icons/fa";
|
||||||
|
|
||||||
|
// export default function Layout() {
|
||||||
|
// const { toggleLang } = useContext(langContext);
|
||||||
|
// const { user, logout } = useContext(AuthContext);
|
||||||
|
// const handleLogout = () => {
|
||||||
|
// logout();
|
||||||
|
// navigate("/");
|
||||||
|
// };
|
||||||
|
// // const {lang, setLang} = useContext(AuthContext);
|
||||||
|
// // console.log(lang);
|
||||||
|
// const menuConfig = [
|
||||||
|
// {
|
||||||
|
// label: "Dashboards",
|
||||||
|
// basePath: "/app/income",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Income", route: "/app/income" },
|
||||||
|
// // { label: "Expenses", route: "/app/expenses" },
|
||||||
|
// // { label: "Cost per room", route: "/app/cost-per-room" },
|
||||||
|
// // { label: "Budget", route: "/app/budget" },
|
||||||
|
// ],
|
||||||
|
// hidden: user === 1 ? false : true //Solo admin puede ver dashboards,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Expenses to be approved",
|
||||||
|
// basePath: "/app/pending-approval",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Pending approval", route: "/app/pending-approval" },
|
||||||
|
// { label: "Approved", route: "/app/approved" },
|
||||||
|
// { label: "Rejected", route: "/app/rejected" },
|
||||||
|
// ],
|
||||||
|
// hidden: user === 1 ? false : true
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Expenses",
|
||||||
|
// basePath: "/app/report-expense",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Report", route: "/app/report-expense" },
|
||||||
|
// { label: "New Expense", route: "/app/new-expense" },
|
||||||
|
// { label: "Payments", route: "/app/payments" },
|
||||||
|
// { label: "Monthly Payments", route: "/app/monthly-payments" },
|
||||||
|
// { label: "New Monthly Payments", route: "/app/new-monthly" },
|
||||||
|
// { label: "Purchase Entries", route: "/app/purchase-entries" },
|
||||||
|
// ],
|
||||||
|
// hidden: user >= 1 && user <= 3 ? false : true
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Inventory",
|
||||||
|
// basePath: "/app/products",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Products", route: "/app/products", hidden: user === 5 ? true : false },
|
||||||
|
// { label: "New Product", route: "/app/new-product", hidden: user === 5 ? true : false },
|
||||||
|
// { label: "Report", route: "/app/inventory-report", hidden: user === 5 ? true : false },
|
||||||
|
// { label: "Discard Product", route: "/app/discard-product", hidden: user === 5 ? true : false },
|
||||||
|
// { label: "Adjustments", route: "/app/product-adjustments", hidden: user === 5 ? true : false }
|
||||||
|
// ],
|
||||||
|
// hidden: user >= 1 && user <= 4 ? false : true
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Payroll",
|
||||||
|
// basePath: "/app/payroll",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Report", route: "/app/payroll" },
|
||||||
|
// { label: "New Contract", route: "/app/payroll/NewPayRoll"},
|
||||||
|
// { label: "Attendance", route: "/app/payroll/attendance" },
|
||||||
|
// { label: "Employees", route: "/app/payroll/employees" },
|
||||||
|
// { label: "New Employee", route: "/app/payroll/newemployee" },
|
||||||
|
// ],
|
||||||
|
// hidden: user >= 1 && user <= 3 ? false : true
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: "Hotel",
|
||||||
|
// basePath: "/app/properties",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "Properties", route: "/app/properties" },
|
||||||
|
// ],
|
||||||
|
// hidden: user === 1 ? false : true
|
||||||
|
// },
|
||||||
|
// //SECCIÓN "OCULTA" PARA SETTINGS
|
||||||
|
// {
|
||||||
|
// label: "Settings",
|
||||||
|
// basePath: "/app/settings",
|
||||||
|
// submenu: [
|
||||||
|
// { label: "General", route: "/app/settings" },
|
||||||
|
// { label: "Users", route: "/app/settings/users" },
|
||||||
|
// { label: "Units", route: "/app/settings/roles" },
|
||||||
|
// { label: "Room management", route: "/app/settings/room-management" },
|
||||||
|
// ],
|
||||||
|
// hidden: true, //etiqueta para ignorar en sidebar
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
// const location = useLocation();
|
||||||
|
// const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
// const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
||||||
|
// const isDetailPage = /^\/app\/properties\/\d+$/.test(location.pathname);
|
||||||
|
|
||||||
|
// //Identificar la sección activa, incluyendo settings
|
||||||
|
// const activeSection = menuConfig.find(section =>
|
||||||
|
// section.submenu.some(item => location.pathname.startsWith(item.route))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const activeSubmenu = activeSection?.submenu || [];
|
||||||
|
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="dashboard-layout">
|
||||||
|
// {/* Sidebar */}
|
||||||
|
// {isSidebarOpen && (
|
||||||
|
// <aside className="sidebar">
|
||||||
|
// <nav>
|
||||||
|
// {/*sSolo se muestran secciones que no están ocultas */}
|
||||||
|
// {menuConfig
|
||||||
|
// .filter(section => !section.hidden)
|
||||||
|
// .map((section, index) => (
|
||||||
|
// <NavLink key={index} to={section.basePath}>
|
||||||
|
// {section.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </nav>
|
||||||
|
// </aside>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Main content */}
|
||||||
|
// <div className="main-content">
|
||||||
|
// {/* Topbar */}
|
||||||
|
// <div className="topbar">
|
||||||
|
// <div className="topbar-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
|
||||||
|
// {/* Botón + Título (alineados a la izquierda) */}
|
||||||
|
// <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
// <button onClick={toggleSidebar} className="sidebar-toggle-button">
|
||||||
|
// ☰
|
||||||
|
// </button>
|
||||||
|
|
||||||
|
// <div className="topbar-title">{activeSection?.label}</div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
|
||||||
|
// {/* Iconos a la derecha */}
|
||||||
|
// <div className="topbar-icons" style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||||
|
// <FaBell className="topbar-icon" />
|
||||||
|
// <FaCog
|
||||||
|
// className="topbar-icon cursor-pointer"
|
||||||
|
// onClick={() => navigate("/app/settings")}
|
||||||
|
// />
|
||||||
|
// <select
|
||||||
|
|
||||||
|
|
||||||
|
// className="language-select"
|
||||||
|
// onChange={toggleLang}
|
||||||
|
// >
|
||||||
|
// <option value="en">EN</option>
|
||||||
|
// <option value="es">ES</option>
|
||||||
|
// </select>
|
||||||
|
// <button
|
||||||
|
// onClick={handleLogout}
|
||||||
|
// style={{
|
||||||
|
// display: "flex",
|
||||||
|
// alignItems: "center",
|
||||||
|
// gap: "6px",
|
||||||
|
// background: "transparent",
|
||||||
|
// color: "#ffffffff",
|
||||||
|
// border: "none",
|
||||||
|
// fontWeight: "bold",
|
||||||
|
// cursor: "pointer"
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <FaSignOutAlt className="topbar-icon" />
|
||||||
|
// </button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Submenú (solo si no es página de detalle) */}
|
||||||
|
// {!isDetailPage && (
|
||||||
|
// <div className="topbar-submenu">
|
||||||
|
// {activeSubmenu.filter(section => !section.hidden).map((item, index) => (
|
||||||
|
// <NavLink
|
||||||
|
// key={index}
|
||||||
|
// to={item.route}
|
||||||
|
// className={({ isActive }) =>
|
||||||
|
// isActive ? "submenu-link active" : "submenu-link"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {item.label}
|
||||||
|
// </NavLink>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
|
||||||
|
// {/* Página actual */}
|
||||||
|
// <div className="content">
|
||||||
|
// <Outlet />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/* components/Modals/ConfirmationModal.css */
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(122, 0, 41, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 450px;
|
||||||
|
max-width: 95%;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
|
||||||
|
border: 5px solid #7a0029;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
background-color: #7a0029;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-button {
|
||||||
|
padding: 10px 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 2px solid #7a0029;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-button.yes {
|
||||||
|
color: green;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-button.no {
|
||||||
|
color: #7a0029;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// components/Modals/ConfirmationModal.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import './ConfirmationModal.css'; // Estilos separados
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { langContext } from '../../context/LenguageContext';
|
||||||
|
|
||||||
|
export default function ConfirmationModal({ isOpen, statusType, onConfirm, onCancel }) {
|
||||||
|
const { lang } = useContext(langContext);
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h3>{lang === "en" ? "Confirm the status change" : "Confirmar el cambio de estado"}</h3>
|
||||||
|
<button className="close-button" onClick={onCancel}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<p>{lang === "en" ? "Are you sure you received" : "¿Estás seguro de que recibiste"} "{statusType}"?</p>
|
||||||
|
<div className="modal-buttons">
|
||||||
|
<button className="modal-button yes" onClick={onConfirm}>{lang === "en" ? "YES" : "SÍ"}</button>
|
||||||
|
<button className="modal-button no" onClick={onCancel}>{lang === "en" ? "NO" : "NO"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// components/Modals/ConfirmationModal.jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './ConfirmationModal.css'; // Estilos separados
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { langContext } from '../../context/LenguageContext';
|
||||||
|
|
||||||
|
|
||||||
|
export default function ConfirmationOutcome({ isOpen, onConfirm, onCancel, description, taxes, initialAmount, initialTaxId, isFixedPayment }) {
|
||||||
|
/*const [formHousekepeer, setFormHousekepeer] = useState(null)/*/
|
||||||
|
const { lang } = useContext(langContext);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
tax: '',
|
||||||
|
amount: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setForm({
|
||||||
|
tax: initialTaxId || '',
|
||||||
|
amount: initialAmount || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen, initialAmount, initialTaxId]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
//console.log(name, value);
|
||||||
|
setForm((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmClick = () => {
|
||||||
|
// Envía los valores al padre (Outcomes.jsx)
|
||||||
|
onConfirm(form.tax, form.amount);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<form onSubmit={handleConfirmClick}>
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h3>{lang === "en" ? "Confirm the status change" : "Confirmar el cambio de estado"}</h3>
|
||||||
|
<button className="close-button" onClick={onCancel}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div>
|
||||||
|
<p>{lang === "en" ? "Payment" : "Pago"}</p>
|
||||||
|
<input name = "PCO" value={description} onChange={handleChange} disabled={true}></input>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{lang === "en" ? "Amount" : "Subtotal"}</p>
|
||||||
|
<input type='number' required name = "amount" value={form.amount} onChange={handleChange} disabled={isFixedPayment}></input>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{lang === "en" ? "Tax" : "Impuesto"}</p>
|
||||||
|
<select name = "tax" required value={form.tax} onChange={handleChange} disabled={isFixedPayment}>
|
||||||
|
<option value="">{lang === "en" ? "Select a tax" : "Selecciona un impuesto"}</option>
|
||||||
|
{taxes?.map(tax => (
|
||||||
|
<option key={tax.id} value={tax.id}>{tax.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="modal-buttons">
|
||||||
|
<button className="modal-button yes" type='submit'>{lang === "en" ? "PAID" : "PAGAR"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// components/Modals/ConfirmationModal.jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './ConfirmationModal.css'; // Estilos separados
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { langContext } from '../../context/LenguageContext';
|
||||||
|
|
||||||
|
|
||||||
|
export default function ConfirmationOutcome({ isOpen, onConfirm, onCancel, formHousekepeer, idproduct, nameProduct, productStock }) {
|
||||||
|
/*const [formHousekepeer, setFormHousekepeer] = useState(null)/*/
|
||||||
|
const { lang } = useContext(langContext);
|
||||||
|
if (!isOpen) return null;
|
||||||
|
const [PCO, setProduct] = useState(idproduct);
|
||||||
|
const [UCO, setUnits] = useState(null);
|
||||||
|
const [HCO, setHousekeeper] = useState(null);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
PCO: idproduct || '',
|
||||||
|
UCO: '',
|
||||||
|
HCO: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
if (name === 'UCO') {
|
||||||
|
const numericValue = Number(value);
|
||||||
|
const maxStock = Number(productStock) || 0;
|
||||||
|
if (value === '') {
|
||||||
|
setForm((prev) => ({ ...prev, [name]: '' }));
|
||||||
|
} else if (!isNaN(numericValue) && numericValue >= 0) {
|
||||||
|
const cappedValue = numericValue > maxStock ? maxStock : numericValue;
|
||||||
|
setForm((prev) => ({ ...prev, [name]: cappedValue.toString() }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setForm((prev) => ({ ...prev, [name]: value }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmClick = () => {
|
||||||
|
// Envía los valores al padre (Outcomes.jsx)
|
||||||
|
onConfirm(form.PCO, form.UCO, form.HCO);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h3>{lang === "en" ? "Confirm the status change" : "Confirmar el cambio de estado"}</h3>
|
||||||
|
<button className="close-button" onClick={onCancel}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div>
|
||||||
|
<p>{lang === "en" ? "Product" : "Producto"}</p>
|
||||||
|
<input
|
||||||
|
name="PCO"
|
||||||
|
value={nameProduct}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={true}
|
||||||
|
style={{ color: '#000', width: '100%', padding: '8px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{lang === "en" ? "Units" : "Unidades"}</p>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
name="UCO"
|
||||||
|
value={form.UCO}
|
||||||
|
min={0}
|
||||||
|
max={productStock ? Number(productStock) : undefined}
|
||||||
|
disabled={!productStock || Number(productStock) <= 0}
|
||||||
|
title={(!productStock || Number(productStock) <= 0) ? (lang === "es" ? "No se puede consumir cuando el stock es 0" : "Cannot consume when stock is 0") : (lang === "es" ? `Máximo disponible: ${productStock}` : `Maximum available: ${productStock}`)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === '-' || e.key === 'e' || e.key === 'E' || e.key === '+') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{
|
||||||
|
color: '#000',
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px',
|
||||||
|
backgroundColor: (!productStock || Number(productStock) <= 0) ? '#f0f0f0' : 'white',
|
||||||
|
cursor: (!productStock || Number(productStock) <= 0) ? 'not-allowed' : 'text',
|
||||||
|
opacity: (!productStock || Number(productStock) <= 0) ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{(!productStock || Number(productStock) <= 0) && (
|
||||||
|
<p style={{ color: 'red', fontSize: '12px', marginTop: '5px', marginBottom: 0 }}>
|
||||||
|
{lang === "es" ? "No hay stock disponible" : "There is no stock"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{lang === "en" ? "Housekeeper" : "Camarista"}</p>
|
||||||
|
<select
|
||||||
|
name="HCO"
|
||||||
|
value={form.HCO}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ color: '#000', width: '100%', padding: '8px' }}
|
||||||
|
>
|
||||||
|
<option value="">{lang === "en" ? "Select a Housekepeer" : "Selecciona una ama de llaves"}</option>
|
||||||
|
{formHousekepeer && formHousekepeer.map(HK => (
|
||||||
|
<option key={HK.rfc_employee} value={HK.rfc_employee}>{HK.name_emp}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="modal-buttons">
|
||||||
|
<button
|
||||||
|
className="modal-button yes"
|
||||||
|
onClick={handleConfirmClick}
|
||||||
|
disabled={!productStock || Number(productStock) <= 0}
|
||||||
|
style={{
|
||||||
|
opacity: (!productStock || Number(productStock) <= 0) ? 0.5 : 1,
|
||||||
|
cursor: (!productStock || Number(productStock) <= 0) ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lang === "en" ? "SAVE" : "GUARDAR"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
.discard-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-box {
|
||||||
|
background: white;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 450px;
|
||||||
|
max-width: 90%;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-header {
|
||||||
|
background-color: #7a0029;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #213547;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-body p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-button {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-button-cancel {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-button-cancel:hover {
|
||||||
|
background-color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-button-confirm {
|
||||||
|
background-color: #7a0029;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-button-confirm:hover {
|
||||||
|
background-color: #5a001f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-modal-button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { langContext } from '../../context/LenguageContext';
|
||||||
|
import './DiscardConfirmModal.css';
|
||||||
|
|
||||||
|
export default function DiscardConfirmModal({ isOpen, message, onConfirm, onCancel }) {
|
||||||
|
const { lang } = useContext(langContext);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="discard-modal-overlay" onClick={onCancel}>
|
||||||
|
<div className="discard-modal-box" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="discard-modal-header">
|
||||||
|
<h3>{lang === "es" ? "Confirmar descarte" : "Confirm Discard"}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="discard-modal-body">
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className="discard-modal-actions">
|
||||||
|
<button className="discard-modal-button discard-modal-button-cancel" onClick={onCancel}>
|
||||||
|
{lang === "es" ? "Cancelar" : "Cancel"}
|
||||||
|
</button>
|
||||||
|
<button className="discard-modal-button discard-modal-button-confirm" onClick={onConfirm}>
|
||||||
|
{lang === "es" ? "Confirmar" : "Confirm"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
57
frontend/Frontend-Hotel/src/components/Modals/Modal.css
Normal file
57
frontend/Frontend-Hotel/src/components/Modals/Modal.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-box {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 350px;
|
||||||
|
max-width: 90%;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-box h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #111;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f1f1f1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary {
|
||||||
|
background-color: #8b0000;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary:hover {
|
||||||
|
background-color: #a00000;
|
||||||
|
}
|
||||||
22
frontend/Frontend-Hotel/src/components/Modals/Modal.jsx
Normal file
22
frontend/Frontend-Hotel/src/components/Modals/Modal.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './Modal.css'; // Asegúrate de tener el estilo del modal
|
||||||
|
|
||||||
|
export default function Modal({ isOpen, closeModal }) {
|
||||||
|
return (
|
||||||
|
isOpen && (
|
||||||
|
<div className="modal-overlay">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3>Enter your email address and we'll send a new password to your email.</h3>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
className="modal-input"
|
||||||
|
/>
|
||||||
|
<button onClick={closeModal} className="btn btn--primary">
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
53
frontend/Frontend-Hotel/src/components/Navbar/Navbar.css
Normal file
53
frontend/Frontend-Hotel/src/components/Navbar/Navbar.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
z/* src/styles/Navbar.css */
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #4a0d0d; /* Marrón oscuro */
|
||||||
|
padding: 12px 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__brand {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link.active {
|
||||||
|
border-bottom: 2px solid #f8d47b; /* Amarillo suave */
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary {
|
||||||
|
background-color: #f8d47b; /* Amarillo */
|
||||||
|
color: #4a0d0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--secondary {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
33
frontend/Frontend-Hotel/src/components/Navbar/Navbar.jsx
Normal file
33
frontend/Frontend-Hotel/src/components/Navbar/Navbar.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// src/components/Navbar.jsx
|
||||||
|
import { NavLink, useNavigate } from "react-router-dom";
|
||||||
|
import { useAuth } from "../../context/AuthContext.jsx";
|
||||||
|
import "./Navbar.css"; // estilos separados
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
const { isAuthenticated, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
navigate("/"); // 👈 al cerrar sesión vuelve al Login
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="navbar">
|
||||||
|
<div className="navbar__brand"></div>
|
||||||
|
|
||||||
|
<div className="navbar__actions">
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<button className="btn btn--secondary" onClick={handleLogout}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<NavLink to="/" className="btn btn--primary">
|
||||||
|
Login
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
53
frontend/Frontend-Hotel/src/components/Sidebar.jsx
Normal file
53
frontend/Frontend-Hotel/src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Sidebar.jsx
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { menuConfig } from "../constants/menuConfig";
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="sidebar">
|
||||||
|
<nav>
|
||||||
|
{Object.entries(menuConfig).map(([key, section]) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
className="sidebar-link"
|
||||||
|
onClick={() => navigate(section.basePath)}
|
||||||
|
>
|
||||||
|
{section.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
|
|
||||||
|
// import React, { useState } from "react";
|
||||||
|
// import { NavLink } from "react-router-dom";
|
||||||
|
// import "./..styles/Sidebar.css";
|
||||||
|
|
||||||
|
// export default function Sidebar() {
|
||||||
|
// const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className={`sidebar ${collapsed ? "collapsed" : ""}`}>
|
||||||
|
// <div className="sidebar-header">
|
||||||
|
// <button className="toggle-btn" onClick={() => setCollapsed(!collapsed)}>
|
||||||
|
// ☰
|
||||||
|
// </button>
|
||||||
|
// {!collapsed && <span className="title">Dashboard</span>}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <nav className="sidebar-nav">
|
||||||
|
// <NavLink to="/app/dashboard">Dashboards</NavLink>
|
||||||
|
// <NavLink to="/app/expenses-to-approve">Expenses to be approved</NavLink>
|
||||||
|
// <NavLink to="/app/expenses">Expenses</NavLink>
|
||||||
|
// <NavLink to="/app/inventory">Inventory</NavLink>
|
||||||
|
// <NavLink to="/app/payroll">Payroll</NavLink>
|
||||||
|
// <NavLink to="/app/hotel">Hotel</NavLink>
|
||||||
|
// </nav>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
100
frontend/Frontend-Hotel/src/components/SummaryCard.css
Normal file
100
frontend/Frontend-Hotel/src/components/SummaryCard.css
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
.summary-card-enhanced {
|
||||||
|
background: linear-gradient(145deg, #ffffff, #f8f9fa);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
min-width: 180px;
|
||||||
|
flex: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card-enhanced:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card-enhanced.primary {
|
||||||
|
background: linear-gradient(145deg, #ffffff, #f8f9fa);
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-amount {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin: 8px 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-percentage {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #28a745;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-top-color: #fcd200;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.summary-card-enhanced {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-amount {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.summary-card-enhanced {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-amount {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
31
frontend/Frontend-Hotel/src/components/SummaryCard.jsx
Normal file
31
frontend/Frontend-Hotel/src/components/SummaryCard.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './SummaryCard.css';
|
||||||
|
|
||||||
|
const SummaryCard = ({
|
||||||
|
title,
|
||||||
|
amount,
|
||||||
|
isLoading = false,
|
||||||
|
isPrimary = false
|
||||||
|
}) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={`summary-card-enhanced ${isPrimary ? 'primary' : ''}`}>
|
||||||
|
<div className="card-loading">
|
||||||
|
<div className="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`summary-card-enhanced ${isPrimary ? 'primary' : ''}`}>
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">{title}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-amount">${amount || 0}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SummaryCard;
|
||||||
|
|
||||||
28
frontend/Frontend-Hotel/src/components/Switch.jsx
Normal file
28
frontend/Frontend-Hotel/src/components/Switch.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Switch = ({ checked, onChange, disabled = false }) => {
|
||||||
|
return (
|
||||||
|
<label className="inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="sr-only"
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`relative inline-block w-10 h-6 transition duration-200 ease-in-out rounded-full ${
|
||||||
|
checked ? 'bg-green-500' : 'bg-gray-300'
|
||||||
|
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ease-in-out ${
|
||||||
|
checked ? 'translate-x-4' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Switch;
|
||||||
152
frontend/Frontend-Hotel/src/components/Table/HotelTable copy.jsx
Normal file
152
frontend/Frontend-Hotel/src/components/Table/HotelTable copy.jsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { FaExternalLinkAlt } from 'react-icons/fa';
|
||||||
|
import './Table.css';
|
||||||
|
|
||||||
|
export default function Table({ columns, data }) {
|
||||||
|
return (
|
||||||
|
<table className="custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<th key={col.key}>{col.header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((row, rowIndex) => (
|
||||||
|
<tr key={rowIndex}>
|
||||||
|
{columns.map((col, colIndex) => (
|
||||||
|
<td key={colIndex}>
|
||||||
|
{col.render
|
||||||
|
? col.render(row[col.key], row, rowIndex)
|
||||||
|
: col.key === 'propertyId'
|
||||||
|
? (
|
||||||
|
<Link
|
||||||
|
to={`/app/properties/${row[col.key]}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: '#003366',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row[col.key]}
|
||||||
|
<FaExternalLinkAlt size={12} />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
: row[col.key]}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// import React from 'react';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
// import { FaArrowRight } from 'react-icons/fa';
|
||||||
|
// import './Table.css';
|
||||||
|
|
||||||
|
// export default function Table({ columns, data }) {
|
||||||
|
// return (
|
||||||
|
// <table className="custom-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((col) => (
|
||||||
|
// <th key={col.key}>{col.header}</th>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {data.map((row, rowIndex) => (
|
||||||
|
// <tr key={rowIndex}>
|
||||||
|
// {columns.map((col, colIndex) => (
|
||||||
|
// <td key={colIndex}>
|
||||||
|
// {col.render
|
||||||
|
// ? col.render(row[col.key], row)
|
||||||
|
// : col.key === 'propertyId'
|
||||||
|
// ? (
|
||||||
|
// <Link to={`/app/properties/${row[col.key]}`} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
// {row[col.key]}
|
||||||
|
// <FaArrowRight size={12} />
|
||||||
|
// </Link>
|
||||||
|
// )
|
||||||
|
// : row[col.key]}
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// import React from 'react';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
// import './Table.css';
|
||||||
|
|
||||||
|
// export default function Table({ columns, data }) {
|
||||||
|
// return (
|
||||||
|
// <table className="custom-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((col) => (
|
||||||
|
// <th key={col.key}>{col.header}</th>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {data.map((row, rowIndex) => (
|
||||||
|
// <tr key={rowIndex}>
|
||||||
|
// {columns.map((col, colIndex) => (
|
||||||
|
// <td key={colIndex}>
|
||||||
|
// {col.render
|
||||||
|
// ? col.render(row[col.key], row) // usar render si está definido
|
||||||
|
// : col.key === 'propertyId'
|
||||||
|
// ? <Link to={`/app/properties/${row[col.key]}`}>{row[col.key]}</Link>
|
||||||
|
// : row[col.key]}
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// import React from 'react';
|
||||||
|
// import './Table.css';
|
||||||
|
|
||||||
|
// export default function Table({ columns, data }) {
|
||||||
|
// return (
|
||||||
|
// <table className="custom-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((col) => (
|
||||||
|
// <th key={col.key}>{col.header}</th>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {data.map((row, rowIndex) => (
|
||||||
|
// <tr key={rowIndex}>
|
||||||
|
// {columns.map((col, colIndex) => (
|
||||||
|
// <td key={colIndex}>
|
||||||
|
// {col.render
|
||||||
|
// ? col.render(row[col.key], row) // usar render si está definido
|
||||||
|
// : row[col.key]}
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
|
||||||
|
// </table>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
152
frontend/Frontend-Hotel/src/components/Table/HotelTable.jsx
Normal file
152
frontend/Frontend-Hotel/src/components/Table/HotelTable.jsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { FaExternalLinkAlt } from 'react-icons/fa';
|
||||||
|
import './Table.css';
|
||||||
|
|
||||||
|
export default function Table({ columns, data }) {
|
||||||
|
return (
|
||||||
|
<table className="custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<th key={col.key} style={col.headerStyle || {}}>{col.header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((row, rowIndex) => (
|
||||||
|
<tr key={rowIndex}>
|
||||||
|
{columns.map((col, colIndex) => (
|
||||||
|
<td key={colIndex}>
|
||||||
|
{col.render
|
||||||
|
? col.render(row[col.key], row)
|
||||||
|
: col.key === 'propertyId'
|
||||||
|
? (
|
||||||
|
<Link
|
||||||
|
to={`/app/properties/${row[col.key]}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: '#003366',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row[col.key]}
|
||||||
|
<FaExternalLinkAlt size={12} />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
: row[col.key]}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// import React from 'react';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
// import { FaArrowRight } from 'react-icons/fa';
|
||||||
|
// import './Table.css';
|
||||||
|
|
||||||
|
// export default function Table({ columns, data }) {
|
||||||
|
// return (
|
||||||
|
// <table className="custom-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((col) => (
|
||||||
|
// <th key={col.key}>{col.header}</th>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {data.map((row, rowIndex) => (
|
||||||
|
// <tr key={rowIndex}>
|
||||||
|
// {columns.map((col, colIndex) => (
|
||||||
|
// <td key={colIndex}>
|
||||||
|
// {col.render
|
||||||
|
// ? col.render(row[col.key], row)
|
||||||
|
// : col.key === 'propertyId'
|
||||||
|
// ? (
|
||||||
|
// <Link to={`/app/properties/${row[col.key]}`} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
// {row[col.key]}
|
||||||
|
// <FaArrowRight size={12} />
|
||||||
|
// </Link>
|
||||||
|
// )
|
||||||
|
// : row[col.key]}
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// import React from 'react';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
// import './Table.css';
|
||||||
|
|
||||||
|
// export default function Table({ columns, data }) {
|
||||||
|
// return (
|
||||||
|
// <table className="custom-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((col) => (
|
||||||
|
// <th key={col.key}>{col.header}</th>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {data.map((row, rowIndex) => (
|
||||||
|
// <tr key={rowIndex}>
|
||||||
|
// {columns.map((col, colIndex) => (
|
||||||
|
// <td key={colIndex}>
|
||||||
|
// {col.render
|
||||||
|
// ? col.render(row[col.key], row) // usar render si está definido
|
||||||
|
// : col.key === 'propertyId'
|
||||||
|
// ? <Link to={`/app/properties/${row[col.key]}`}>{row[col.key]}</Link>
|
||||||
|
// : row[col.key]}
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// import React from 'react';
|
||||||
|
// import './Table.css';
|
||||||
|
|
||||||
|
// export default function Table({ columns, data }) {
|
||||||
|
// return (
|
||||||
|
// <table className="custom-table">
|
||||||
|
// <thead>
|
||||||
|
// <tr>
|
||||||
|
// {columns.map((col) => (
|
||||||
|
// <th key={col.key}>{col.header}</th>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// </thead>
|
||||||
|
// <tbody>
|
||||||
|
// {data.map((row, rowIndex) => (
|
||||||
|
// <tr key={rowIndex}>
|
||||||
|
// {columns.map((col, colIndex) => (
|
||||||
|
// <td key={colIndex}>
|
||||||
|
// {col.render
|
||||||
|
// ? col.render(row[col.key], row) // usar render si está definido
|
||||||
|
// : row[col.key]}
|
||||||
|
// </td>
|
||||||
|
// ))}
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
|
||||||
|
// </table>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user