mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 13:15:16 +00:00
feat: Implement AI-powered permit reason classification and add comprehensive project documentation.
This commit is contained in:
13
.env.example
13
.env.example
@@ -1,10 +1,16 @@
|
|||||||
# Configuración de Telegram
|
# Configuración de Telegram
|
||||||
TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
|
TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
|
||||||
|
TELEGRAM_ADMIN_CHAT_ID=TELEGRAM_ADMIN_CHAT_ID
|
||||||
|
OPENAI_API_KEY=SK......
|
||||||
|
GOOGLE_API_KEY=AIzaSyBqH5...
|
||||||
|
|
||||||
|
|
||||||
# Webhooks de n8n (puedes agregar más aquí en el futuro)
|
# Webhooks de n8n (puedes agregar más aquí en el futuro)
|
||||||
WEBHOOK_CONTRATO=
|
WEBHOOK_CONTRATO=url
|
||||||
WEBHOOK_PRINT=
|
WEBHOOK_PRINT=url
|
||||||
WEBHOOK_VACACIONES=
|
WEBHOOK_VACACIONES=url
|
||||||
|
WEBHOOK_PERMISOS=url
|
||||||
|
WEBHOOK_DATA_EMPLEADO=url
|
||||||
|
|
||||||
# --- DATABASE ---
|
# --- DATABASE ---
|
||||||
# Usado por el servicio de la base de datos en docker-compose.yml
|
# Usado por el servicio de la base de datos en docker-compose.yml
|
||||||
@@ -23,4 +29,5 @@ IMAP_SERVER=imap.hostinger.com
|
|||||||
IMAP_PORT=993
|
IMAP_PORT=993
|
||||||
IMAP_USER=your_email@example.com
|
IMAP_USER=your_email@example.com
|
||||||
IMAP_PASSWORD=your_password
|
IMAP_PASSWORD=your_password
|
||||||
|
PRINTER_EMAIL=your_email@example.com
|
||||||
|
|
||||||
|
|||||||
BIN
Screenshot_20251214-112252.png
Normal file
BIN
Screenshot_20251214-112252.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
277
Vanessa.md
Normal file
277
Vanessa.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# 👩💼 Vanessa Bot Brain
|
||||||
|
|
||||||
|
### Vanity / Soul — HR Automation
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Vanessa** es la Asistente Virtual de Recursos Humanos para **Vanity / Soul**. No es solo un bot de comandos: es un **cerebro central modular** diseñado para gestionar el ciclo de vida de las socias —contratación, solicitudes internas y servicios— con una personalidad cálida, eficiente y humana.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Filosofía del Diseño
|
||||||
|
|
||||||
|
Vanessa fue construida bajo tres principios:
|
||||||
|
|
||||||
|
1. **Conversaciones humanas, datos estrictos**
|
||||||
|
La UX es natural; la salida siempre es un **payload JSON inmutable**.
|
||||||
|
|
||||||
|
2. **Estado efímero, persistencia externa**
|
||||||
|
El bot no guarda información sensible. Todo se envía a **n8n + Base de Datos**.
|
||||||
|
|
||||||
|
3. **Modularidad real**
|
||||||
|
Cada habilidad vive en su propio archivo y puede evolucionar sin romper el sistema.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Arquitectura del Sistema
|
||||||
|
|
||||||
|
Arquitectura **modular y desacoplada**:
|
||||||
|
|
||||||
|
* **Cerebro (`main.py`)**
|
||||||
|
Orquesta Telegram, gestiona sesiones y enruta comandos.
|
||||||
|
|
||||||
|
* **Habilidades (`/modules`)**
|
||||||
|
Cada módulo implementa un flujo conversacional completo.
|
||||||
|
|
||||||
|
* **Inteligencia Artificial**
|
||||||
|
OpenAI o Gemini para clasificación semántica y entendimiento de texto libre.
|
||||||
|
|
||||||
|
* **Persistencia (n8n + DB)**
|
||||||
|
Recepción de eventos mediante Webhooks con UUID y timestamp.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Estructura del Proyecto
|
||||||
|
|
||||||
|
```text
|
||||||
|
/vanity_bot_brain
|
||||||
|
│
|
||||||
|
├── .env # Credenciales y Webhooks
|
||||||
|
├── main.py # Cerebro / Orquestador
|
||||||
|
├── requirements.txt # Dependencias
|
||||||
|
├── README.md # Documentación
|
||||||
|
│
|
||||||
|
└── modules/ # HABILIDADES
|
||||||
|
├── __init__.py
|
||||||
|
├── onboarding.py # /welcome — Contrato (35 pasos)
|
||||||
|
├── rh_requests.py # /vacaciones y /permiso (IA)
|
||||||
|
└── printer.py # /print — Envío de archivos
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Módulos y Flujos Conversacionales
|
||||||
|
|
||||||
|
### 1️⃣ Onboarding — `/welcome`
|
||||||
|
|
||||||
|
**Objetivo**
|
||||||
|
Recopilar la información completa para el contrato de nuevas socias.
|
||||||
|
|
||||||
|
**Características clave**
|
||||||
|
|
||||||
|
* Flujo guiado de **35 pasos**
|
||||||
|
* Normalización de RFC y CURP
|
||||||
|
* Validación de fechas
|
||||||
|
* Teclados dinámicos
|
||||||
|
* Referencias personales en bucle
|
||||||
|
|
||||||
|
**Ejemplo de conversación**
|
||||||
|
|
||||||
|
```
|
||||||
|
User: /welcome
|
||||||
|
Vanessa: ¡Hola Ana! 👋 Soy Vanessa de RH. Vamos a dejar listo tu registro.
|
||||||
|
Vanessa: ¿Cómo te gusta que te llamemos?
|
||||||
|
User: Anita
|
||||||
|
Vanessa: Perfecto ✨ ¿Cuál es tu nombre completo como aparece en tu INE?
|
||||||
|
...
|
||||||
|
Vanessa: ¡Listo! ✅ Tu información ya está en el sistema. Bienvenida a Vanity.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Payload enviado a n8n**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"candidato": {
|
||||||
|
"nombre_oficial": "ANA MARIA PEREZ",
|
||||||
|
"rfc": "PEQA901010...",
|
||||||
|
"curp": "PEQA901010...",
|
||||||
|
"fecha_nacimiento": "1990-10-10"
|
||||||
|
},
|
||||||
|
"laboral": {
|
||||||
|
"rol_id": "manager",
|
||||||
|
"sucursal_id": "plaza_cima",
|
||||||
|
"fecha_inicio": "2026-01-15"
|
||||||
|
},
|
||||||
|
"referencias": [
|
||||||
|
{ "nombre": "Juan", "telefono": "555...", "relacion": "Trabajo" },
|
||||||
|
{ "nombre": "Luisa", "telefono": "844...", "relacion": "Familiar" }
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2025-12-14T10:00:00-06:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ Vacaciones — `/vacaciones`
|
||||||
|
|
||||||
|
**Objetivo**
|
||||||
|
Gestionar descansos aplicando reglas de negocio automáticamente.
|
||||||
|
|
||||||
|
**Semáforo de decisión**
|
||||||
|
|
||||||
|
* 🔴 Menos de 5 días → Rechazo automático
|
||||||
|
* 🟡 6 a 11 días → Revisión manual
|
||||||
|
* 🟢 12+ días → Pre-aprobado
|
||||||
|
|
||||||
|
**Ejemplo**
|
||||||
|
|
||||||
|
```
|
||||||
|
Vanessa: Días solicitados: 6
|
||||||
|
Vanessa: Anticipación: 35 días
|
||||||
|
🟢 Excelente planeación. Solicitud pre-aprobada.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Payload generado**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"record_id": "uuid-v4-unico",
|
||||||
|
"tipo_solicitud": "VACACIONES",
|
||||||
|
"fechas": {
|
||||||
|
"inicio": "2026-01-20",
|
||||||
|
"fin": "2026-01-25"
|
||||||
|
},
|
||||||
|
"metricas": {
|
||||||
|
"dias_totales": 6,
|
||||||
|
"dias_anticipacion": 35
|
||||||
|
},
|
||||||
|
"status_inicial": "PRE_APROBADO",
|
||||||
|
"created_at": "2025-12-14T10:05:00-06:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ Permisos con IA — `/permiso`
|
||||||
|
|
||||||
|
**Objetivo**
|
||||||
|
Registrar incidencias, salidas o permisos cortos.
|
||||||
|
|
||||||
|
**IA aplicada**
|
||||||
|
El bot analiza el texto libre y clasifica el motivo:
|
||||||
|
|
||||||
|
* EMERGENCIA
|
||||||
|
* MÉDICO
|
||||||
|
* TRÁMITE
|
||||||
|
* PERSONAL
|
||||||
|
|
||||||
|
**Ejemplo**
|
||||||
|
|
||||||
|
```
|
||||||
|
Usuario: Mi hijo se cayó en la escuela
|
||||||
|
Vanessa: Categoría detectada → EMERGENCIA 🚨
|
||||||
|
```
|
||||||
|
|
||||||
|
**Payload**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"record_id": "uuid-v4-unico",
|
||||||
|
"tipo_solicitud": "PERMISO",
|
||||||
|
"motivo_usuario": "Mi hijo se cayó en la escuela...",
|
||||||
|
"categoria_detectada": "EMERGENCIA",
|
||||||
|
"fechas": {
|
||||||
|
"inicio": "2025-12-15",
|
||||||
|
"fin": "2025-12-15"
|
||||||
|
},
|
||||||
|
"created_at": "2025-12-14T10:10:00-06:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ Impresión — `/print`
|
||||||
|
|
||||||
|
**Objetivo**
|
||||||
|
Enviar documentos directamente a la cola de impresión de la oficina.
|
||||||
|
|
||||||
|
**Soporta**
|
||||||
|
|
||||||
|
* PDF
|
||||||
|
* Word
|
||||||
|
* Imágenes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Instalación y Ejecución con Docker
|
||||||
|
|
||||||
|
### Requisitos
|
||||||
|
|
||||||
|
* Docker
|
||||||
|
* Docker Compose
|
||||||
|
|
||||||
|
### 1. Configuración del Entorno
|
||||||
|
|
||||||
|
Antes de iniciar, es necesario configurar las variables de entorno.
|
||||||
|
|
||||||
|
1. **Crear el archivo `.env`**: Copia el archivo de ejemplo `.env.example` y renómbralo a `.env`.
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
2. **Editar las variables**: Abre el archivo `.env` y rellena todas las variables requeridas:
|
||||||
|
* `TELEGRAM_TOKEN`: El token de tu bot de Telegram.
|
||||||
|
* `GOOGLE_API_KEY`: Tu clave de API de Google para la IA de Gemini.
|
||||||
|
* `WEBHOOK_*`: Las URLs de los webhooks de tu sistema de automatización (ej. n8n).
|
||||||
|
* `MYSQL_*`: Las credenciales para la base de datos (puedes dejar las que vienen por defecto si solo es para desarrollo local).
|
||||||
|
* `SMTP_*`: Las credenciales de tu servidor de correo para la función de impresión.
|
||||||
|
|
||||||
|
### 2. Construcción y Ejecución
|
||||||
|
|
||||||
|
Una vez configurado el entorno, el proyecto se gestiona fácilmente con Docker Compose.
|
||||||
|
|
||||||
|
1. **Construir las imágenes**: Este comando crea las imágenes de Docker para el bot y la base de datos.
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
2. **Iniciar los servicios**: Este comando inicia el bot y la base de datos en segundo plano.
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
El bot ahora estará en funcionamiento. Para detener los servicios, puedes usar `docker-compose down`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Esquema de Base de Datos Sugerido
|
||||||
|
|
||||||
|
Tabla: **rh_solicitudes**
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripción |
|
||||||
|
| ----------------- | --------- | ---------------------- |
|
||||||
|
| record_id | UUID | Identificador único |
|
||||||
|
| user_id | BIGINT | Telegram ID |
|
||||||
|
| nombre | VARCHAR | Nombre del colaborador |
|
||||||
|
| tipo | VARCHAR | VACACIONES / PERMISO |
|
||||||
|
| fechas | JSON | Rango de fechas |
|
||||||
|
| motivo | TEXT | Texto original |
|
||||||
|
| categoria | VARCHAR | Clasificación IA |
|
||||||
|
| dias_anticipacion | INT | Métrica RH |
|
||||||
|
| status_bot | VARCHAR | Resultado automático |
|
||||||
|
| created_at | TIMESTAMP | Zona MTY |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Extensibilidad
|
||||||
|
|
||||||
|
Para agregar un nuevo comando:
|
||||||
|
|
||||||
|
1. Crear un archivo en `/modules`
|
||||||
|
2. Implementar su flujo
|
||||||
|
3. Registrar el comando en `main.py`
|
||||||
|
|
||||||
|
Vanessa ya sabe pensar. Solo enséñale una nueva habilidad. 🧠
|
||||||
@@ -7,8 +7,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
|
||||||
- DATABASE_URL=mysql+mysqlconnector://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
41
modules/ai.py
Normal file
41
modules/ai.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import os
|
||||||
|
import google.generativeai as genai
|
||||||
|
|
||||||
|
def classify_reason(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Clasifica el motivo de un permiso utilizando la API de Gemini.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: El motivo del permiso proporcionado por el usuario.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
La categoría clasificada (EMERGENCIA, MÉDICO, TRÁMITE, PERSONAL) o "OTRO" si no se puede clasificar.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
||||||
|
|
||||||
|
model = genai.GenerativeModel('gemini-pro')
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Clasifica el siguiente motivo de solicitud de permiso en una de estas cuatro categorías: EMERGENCIA, MÉDICO, TRÁMITE, PERSONAL.
|
||||||
|
Responde únicamente con la palabra de la categoría en mayúsculas.
|
||||||
|
|
||||||
|
Motivo: "{text}"
|
||||||
|
Categoría:
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = model.generate_content(prompt)
|
||||||
|
|
||||||
|
# Limpiar la respuesta para obtener solo la categoría
|
||||||
|
category = response.text.strip().upper()
|
||||||
|
|
||||||
|
# Validar que la categoría sea una de las esperadas
|
||||||
|
valid_categories = ["EMERGENCIA", "MÉDICO", "TRÁMITE", "PERSONAL"]
|
||||||
|
if category in valid_categories:
|
||||||
|
return category
|
||||||
|
else:
|
||||||
|
return "PERSONAL" # Si la IA devuelve algo inesperado, se asigna a PERSONAL
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al clasificar con IA: {e}")
|
||||||
|
return "PERSONAL" # En caso de error, se asigna a PERSONAL por defecto
|
||||||
@@ -8,12 +8,21 @@ import logging
|
|||||||
# Configuración de logging
|
# Configuración de logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
# Obtener la URL de la base de datos desde las variables de entorno
|
# Construir la URL de la base de datos desde las variables de entorno individuales
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL", "mysql+mysqlconnector://user:password@db:3306/vanessa_logs")
|
try:
|
||||||
|
user = os.getenv("MYSQL_USER")
|
||||||
|
password = os.getenv("MYSQL_PASSWORD")
|
||||||
|
host = "db" # El nombre del servicio de la base de datos en docker-compose
|
||||||
|
database = os.getenv("MYSQL_DATABASE")
|
||||||
|
DATABASE_URL = f"mysql+mysqlconnector://{user}:{password}@{host}:3306/{database}"
|
||||||
|
|
||||||
# Crear el motor de la base de datos
|
# Crear el motor de la base de datos
|
||||||
engine = create_engine(DATABASE_URL)
|
engine = create_engine(DATABASE_URL)
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
logging.error("Error: Faltan una o más variables de entorno para la base de datos (MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE).")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
# Base para los modelos declarativos
|
# Base para los modelos declarativos
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|||||||
@@ -1,22 +1,69 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import requests
|
import requests
|
||||||
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
|
import uuid
|
||||||
|
from datetime import datetime, date
|
||||||
|
from telegram import Update
|
||||||
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
|
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
|
||||||
from modules.database import log_request
|
from modules.database import log_request
|
||||||
|
from modules.ai import classify_reason
|
||||||
|
|
||||||
|
TIPO_SOLICITITUD, FECHAS, MOTIVO = range(3)
|
||||||
|
|
||||||
|
def _calculate_vacation_metrics(date_string: str) -> dict:
|
||||||
|
"""
|
||||||
|
Calcula métricas de vacaciones a partir de un texto.
|
||||||
|
Asume un formato como "10 al 15 de Octubre".
|
||||||
|
"""
|
||||||
|
today = date.today()
|
||||||
|
current_year = today.year
|
||||||
|
|
||||||
|
# Mapeo de meses en español a número
|
||||||
|
meses = {
|
||||||
|
'enero': 1, 'febrero': 2, 'marzo': 3, 'abril': 4, 'mayo': 5, 'junio': 6,
|
||||||
|
'julio': 7, 'agosto': 8, 'septiembre': 9, 'octubre': 10, 'noviembre': 11, 'diciembre': 12
|
||||||
|
}
|
||||||
|
|
||||||
|
# Regex para "10 al 15 de Octubre"
|
||||||
|
match = re.search(r'(\d{1,2})\s*al\s*(\d{1,2})\s*de\s*(\w+)', date_string, re.IGNORECASE)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
return {"dias_totales": 0, "dias_anticipacion": 0}
|
||||||
|
|
||||||
|
start_day, end_day, month_str = match.groups()
|
||||||
|
start_day, end_day = int(start_day), int(end_day)
|
||||||
|
month = meses.get(month_str.lower())
|
||||||
|
|
||||||
|
if not month:
|
||||||
|
return {"dias_totales": 0, "dias_anticipacion": 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_date = date(current_year, month, start_day)
|
||||||
|
# Si la fecha ya pasó este año, asumir que es del próximo año
|
||||||
|
if start_date < today:
|
||||||
|
start_date = date(current_year + 1, month, start_day)
|
||||||
|
|
||||||
|
end_date = date(start_date.year, month, end_day)
|
||||||
|
|
||||||
|
dias_totales = (end_date - start_date).days + 1
|
||||||
|
dias_anticipacion = (start_date - today).days
|
||||||
|
|
||||||
|
return {"dias_totales": dias_totales, "dias_anticipacion": dias_anticipacion, "fechas_calculadas": {"inicio": start_date.isoformat(), "fin": end_date.isoformat()}}
|
||||||
|
except ValueError:
|
||||||
|
return {"dias_totales": 0, "dias_anticipacion": 0}
|
||||||
|
|
||||||
TIPO_SOLICITUD, FECHAS, MOTIVO = range(3)
|
|
||||||
|
|
||||||
async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "vacaciones", update.message.text)
|
log_request(user.id, user.username, "vacaciones", update.message.text)
|
||||||
context.user_data['tipo'] = 'Vacaciones'
|
context.user_data['tipo'] = 'VACACIONES'
|
||||||
await update.message.reply_text("🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas? (Ej: 10 al 15 de Octubre)")
|
await update.message.reply_text("🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas? (Ej: 10 al 15 de Octubre)")
|
||||||
return FECHAS
|
return FECHAS
|
||||||
|
|
||||||
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "permiso", update.message.text)
|
log_request(user.id, user.username, "permiso", update.message.text)
|
||||||
context.user_data['tipo'] = 'Permiso Especial'
|
context.user_data['tipo'] = 'PERMISO'
|
||||||
await update.message.reply_text("⏱️ **Solicitud de Permiso**\n\n¿Para qué día y horario lo necesitas?")
|
await update.message.reply_text("⏱️ **Solicitud de Permiso**\n\n¿Para qué día y horario lo necesitas?")
|
||||||
return FECHAS
|
return FECHAS
|
||||||
|
|
||||||
@@ -30,24 +77,62 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
datos = context.user_data
|
datos = context.user_data
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
|
|
||||||
# Payload para n8n
|
# Generar payload base
|
||||||
payload = {
|
payload = {
|
||||||
"solicitante": user.full_name,
|
"record_id": str(uuid.uuid4()),
|
||||||
"id_telegram": user.id,
|
"solicitante": {
|
||||||
|
"id_telegram": user.id,
|
||||||
|
"nombre": user.full_name
|
||||||
|
},
|
||||||
"tipo_solicitud": datos['tipo'],
|
"tipo_solicitud": datos['tipo'],
|
||||||
"fechas": datos['fechas'],
|
"fechas_texto_original": datos['fechas'],
|
||||||
"motivo": motivo
|
"motivo_usuario": motivo,
|
||||||
|
"created_at": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
webhook = os.getenv("WEBHOOK_VACACIONES")
|
if datos['tipo'] == 'PERMISO':
|
||||||
|
webhook = os.getenv("WEBHOOK_PERMISOS")
|
||||||
|
categoria = classify_reason(motivo)
|
||||||
|
payload["categoria_detectada"] = categoria
|
||||||
|
await update.message.reply_text(f"Categoría detectada → **{categoria}** 🚨")
|
||||||
|
|
||||||
|
elif datos['tipo'] == 'VACACIONES':
|
||||||
|
webhook = os.getenv("WEBHOOK_VACACIONES")
|
||||||
|
metrics = _calculate_vacation_metrics(datos['fechas'])
|
||||||
|
|
||||||
|
if metrics["dias_totales"] > 0:
|
||||||
|
payload["metricas"] = metrics
|
||||||
|
|
||||||
|
dias = metrics["dias_totales"]
|
||||||
|
if dias <= 5:
|
||||||
|
status = "RECHAZADO"
|
||||||
|
mensaje = f"🔴 {dias} días es un periodo muy corto. Las vacaciones deben ser de al menos 6 días."
|
||||||
|
elif 6 <= dias <= 11:
|
||||||
|
status = "REVISION_MANUAL"
|
||||||
|
mensaje = f"🟡 Solicitud de {dias} días recibida. Tu manager la revisará pronto."
|
||||||
|
else: # 12+
|
||||||
|
status = "PRE_APROBADO"
|
||||||
|
mensaje = f"🟢 ¡Excelente planeación! Tu solicitud de {dias} días ha sido pre-aprobada."
|
||||||
|
|
||||||
|
payload["status_inicial"] = status
|
||||||
|
await update.message.reply_text(mensaje)
|
||||||
|
else:
|
||||||
|
# Si no se pudieron parsear las fechas
|
||||||
|
payload["status_inicial"] = "ERROR_FECHAS"
|
||||||
|
await update.message.reply_text("🤔 No entendí las fechas. Por favor, usa un formato como '10 al 15 de Octubre'.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
requests.post(webhook, json=payload)
|
if webhook:
|
||||||
await update.message.reply_text(f"✅ Solicitud de *{datos['tipo']}* enviada a tu Manager.")
|
requests.post(webhook, json=payload)
|
||||||
except:
|
tipo_solicitud_texto = "Permiso" if datos['tipo'] == 'PERMISO' else 'Vacaciones'
|
||||||
|
await update.message.reply_text(f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error enviando webhook: {e}")
|
||||||
await update.message.reply_text("⚠️ Error enviando la solicitud.")
|
await update.message.reply_text("⚠️ Error enviando la solicitud.")
|
||||||
|
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|
||||||
|
|
||||||
async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
await update.message.reply_text("Solicitud cancelada.")
|
await update.message.reply_text("Solicitud cancelada.")
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ python-dotenv
|
|||||||
requests
|
requests
|
||||||
SQLAlchemy
|
SQLAlchemy
|
||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
|
google-generativeai
|
||||||
Reference in New Issue
Block a user