mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 13:15:16 +00:00
feat: refactor schedule storage to vanity_hr schema, update onboarding command to /registro, and enhance horario flow with short name collection
This commit is contained in:
27
Readme.md
27
Readme.md
@@ -10,11 +10,12 @@ Este repositorio está pensado como **proyecto Python profesional**, modular y l
|
|||||||
|
|
||||||
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
|
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
|
||||||
|
|
||||||
- **Onboarding completo de nuevas socias (`/welcome`)**: Recolecta datos, valida que no existan duplicados en la DB, y ejecuta un registro en dos fases:
|
- **Onboarding completo de nuevas socias (`/registro`, alias `/welcome`)**: Recolecta datos, valida que no existan duplicados en la DB, y ejecuta un registro en dos fases. **Debe completarse una vez para habilitar el resto de comandos.**
|
||||||
1. **Crea un usuario de acceso** en la tabla `USERS_ALMA.users` para la autenticación del bot.
|
1. **Crea un usuario de acceso** en la tabla `USERS_ALMA.users` para la autenticación del bot.
|
||||||
2. **Crea un perfil de empleada** completo en la tabla `vanity_hr.data_empleadas`, que es la tabla maestra de RRHH.
|
2. **Crea un perfil de empleada** completo en la tabla `vanity_hr.data_empleadas`, que es la tabla maestra de RRHH.
|
||||||
- **Solicitud de vacaciones (`/vacaciones`)**: Flujo dinámico para gestionar días de descanso.
|
- **Definición de horario semanal (`/horario`)**: Captura guiada día a día que termina en un upsert por día dentro de `vanity_hr.horario_empleadas` y dispara un webhook operativo; sólo se habilita si ya estás registrada.
|
||||||
- **Solicitud de permisos por horas (`/permiso`)**: Incluye clasificación de motivos mediante IA (Gemini).
|
- **Solicitud de vacaciones (`/vacaciones`)**: Flujo dinámico para gestionar días de descanso, disponible sólo si tu `telegram_id` ya existe en la base vía `/registro`.
|
||||||
|
- **Solicitud de permisos por horas (`/permiso`)**: Incluye clasificación de motivos mediante IA (Gemini) y requiere que el onboarding haya terminado.
|
||||||
|
|
||||||
Cada flujo es un módulo independiente que interactúa con la base de datos y flujos de **n8n**.
|
Cada flujo es un módulo independiente que interactúa con la base de datos y flujos de **n8n**.
|
||||||
|
|
||||||
@@ -38,11 +39,14 @@ vanity_bot/
|
|||||||
│ ├── vanity_hr_models.py
|
│ ├── vanity_hr_models.py
|
||||||
│ └── vanity_attendance_models.py
|
│ └── vanity_attendance_models.py
|
||||||
│
|
│
|
||||||
└── modules/ # Habilidades del bot
|
├── conv-flows/ # Plantillas JSON de flujos declarativos (p. ej. horario.json)
|
||||||
|
└── modules/ # Habilidades del bot y utilidades
|
||||||
├── ai.py # Clasificación de motivos con Gemini
|
├── ai.py # Clasificación de motivos con Gemini
|
||||||
├── database.py # Conexión a DB y lógica de negocio (registro/verificación)
|
├── database.py # Conexión a DB y lógica de negocio (registro/verificación)
|
||||||
|
├── finalizer.py # Acciones finales por flujo (webhooks + persistencia)
|
||||||
|
├── flow_builder.py # Loader que convierte las plantillas JSON en ConversationHandlers
|
||||||
├── logger.py # Registro de auditoría
|
├── logger.py # Registro de auditoría
|
||||||
├── onboarding.py # Flujo /welcome
|
├── onboarding.py # Flujo /registro (/welcome)
|
||||||
├── rh_requests.py # /vacaciones y /permiso
|
├── rh_requests.py # /vacaciones y /permiso
|
||||||
└── ui.py # Teclados y componentes de interfaz
|
└── ui.py # Teclados y componentes de interfaz
|
||||||
```
|
```
|
||||||
@@ -64,6 +68,7 @@ GOOGLE_API_KEY=AIzaSy...
|
|||||||
WEBHOOK_ONBOARDING=https://...
|
WEBHOOK_ONBOARDING=https://...
|
||||||
WEBHOOK_VACACIONES=https://...
|
WEBHOOK_VACACIONES=https://...
|
||||||
WEBHOOK_PERMISOS=https://...
|
WEBHOOK_PERMISOS=https://...
|
||||||
|
WEBHOOK_SCHEDULE=https://...
|
||||||
|
|
||||||
# --- DATABASE SETUP ---
|
# --- DATABASE SETUP ---
|
||||||
MYSQL_HOST=db
|
MYSQL_HOST=db
|
||||||
@@ -79,6 +84,18 @@ MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔄 Flujos declarativos
|
||||||
|
|
||||||
|
El bot puede registrar conversaciones complejas sin código específico gracias a:
|
||||||
|
|
||||||
|
- `conv-flows/*.json`: Describe cada paso del flujo (texto, teclados, variables y transiciones). Actualmente `horario.json` define `/horario`.
|
||||||
|
- `modules/flow_builder.py`: Lee los JSON y crea dinámicamente los `ConversationHandler`.
|
||||||
|
- `modules/finalizer.py`: Ejecuta la acción final de cada flujo. Para `/horario` convierte las horas a formato 24 h, envía `WEBHOOK_SCHEDULE` y distribuye los registros por día en `vanity_hr.horario_empleadas`.
|
||||||
|
|
||||||
|
Si un flujo requiere lógica adicional, se agrega un finalizer nuevo y se anota en el map `FINALIZATION_MAP`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🐳 Ejecución con Docker (Recomendado)
|
## 🐳 Ejecución con Docker (Recomendado)
|
||||||
|
|
||||||
El proyecto está dockerizado para facilitar su despliegue y aislamiento.
|
El proyecto está dockerizado para facilitar su despliegue y aislamiento.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Arquitectura **modular y desacoplada**:
|
|||||||
│
|
│
|
||||||
└── modules/ # HABILIDADES
|
└── modules/ # HABILIDADES
|
||||||
├── __init__.py
|
├── __init__.py
|
||||||
├── onboarding.py # /welcome — Contrato (35 pasos)
|
├── onboarding.py # /registro (/welcome) — Contrato (35 pasos)
|
||||||
├── rh_requests.py # /vacaciones y /permiso (IA)
|
├── rh_requests.py # /vacaciones y /permiso (IA)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ Arquitectura **modular y desacoplada**:
|
|||||||
|
|
||||||
## 💬 Módulos y Flujos Conversacionales
|
## 💬 Módulos y Flujos Conversacionales
|
||||||
|
|
||||||
### 1️⃣ Onboarding — `/welcome`
|
### 1️⃣ Onboarding — `/registro` (alias `/welcome`)
|
||||||
|
|
||||||
**Objetivo**
|
**Objetivo**
|
||||||
Recopilar la información completa para el contrato de nuevas socias.
|
Recopilar la información completa para el contrato de nuevas socias.
|
||||||
@@ -80,7 +80,7 @@ Recopilar la información completa para el contrato de nuevas socias.
|
|||||||
**Ejemplo de conversación**
|
**Ejemplo de conversación**
|
||||||
|
|
||||||
```
|
```
|
||||||
User: /welcome
|
User: /registro
|
||||||
Vanessa: ¡Hola Ana! 👋 Soy Vanessa de RH. Vamos a dejar listo tu registro.
|
Vanessa: ¡Hola Ana! 👋 Soy Vanessa de RH. Vamos a dejar listo tu registro.
|
||||||
Vanessa: ¿Cómo te gusta que te llamemos?
|
Vanessa: ¿Cómo te gusta que te llamemos?
|
||||||
User: Anita
|
User: Anita
|
||||||
|
|||||||
@@ -2,114 +2,118 @@
|
|||||||
"flow_name": "horario",
|
"flow_name": "horario",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"state": 0,
|
"state": -3,
|
||||||
"variable": "INTRO_READ",
|
"variable": "INTRO_READ",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "ANTES DE DEFINIR TUS HORARIOS\n\nLee con atención:\n\n• Estos horarios se usarán para control de asistencia y reportes\n• Selecciona únicamente los botones disponibles\n• El sistema convertirá automáticamente a formato 24 hrs\n\nCuando estés listo, continúa.",
|
"question": "ANTES DE DEFINIR TUS HORARIOS\n\nLee con atención:\n\n• Estos horarios se usarán para control de asistencia y reportes\n• Selecciona únicamente los botones disponibles\n• El sistema convertirá automáticamente a formato 24 hrs\n\nCuando estés lista, confirma para continuar.",
|
||||||
"options": ["Continuar"],
|
"options": ["Continuar"]
|
||||||
"next_step": 1
|
},
|
||||||
|
{
|
||||||
|
"state": -2,
|
||||||
|
"variable": "INTRO_NAME",
|
||||||
|
"type": "info",
|
||||||
|
"question": "NOMBRE CORTO\n\nEste nombre se usará para mensajes internos, reportes y notificaciones."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": -1,
|
||||||
|
"variable": "SHORT_NAME",
|
||||||
|
"type": "text",
|
||||||
|
"question": "¿Cómo te dicen normalmente?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 0,
|
||||||
|
"variable": "INTRO_HOURS",
|
||||||
|
"type": "info",
|
||||||
|
"question": "DEFINICIÓN DE HORARIOS\n\nDefine tu horario habitual por día usando botones AM / PM."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 1,
|
"state": 1,
|
||||||
"variable": "SHORT_NAME",
|
|
||||||
"type": "text",
|
|
||||||
"question": "¿Cómo te dicen normalmente?",
|
|
||||||
"next_step": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"state": 2,
|
|
||||||
"variable": "MONDAY_IN",
|
"variable": "MONDAY_IN",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Lunes · Hora de entrada",
|
"question": "Lunes · Hora de entrada",
|
||||||
"options": ["10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM"],
|
"options": ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM"]
|
||||||
"next_step": 3
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 3,
|
"state": 2,
|
||||||
"variable": "MONDAY_OUT",
|
"variable": "MONDAY_OUT",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Lunes · Hora de salida",
|
"question": "Lunes · Hora de salida",
|
||||||
"options": ["4:00 PM","5:00 PM","6:00 PM","7:00 PM","8:00 PM"],
|
"options": ["4:00 PM", "5:00 PM", "6:00 PM", "7:00 PM", "8:00 PM"]
|
||||||
"next_step": 4
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 4,
|
"state": 3,
|
||||||
"variable": "TUESDAY_IN",
|
"variable": "TUESDAY_IN",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Martes · Hora de entrada",
|
"question": "Martes · Hora de entrada",
|
||||||
"options": ["10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM"],
|
"options": ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM"]
|
||||||
"next_step": 5
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 5,
|
"state": 4,
|
||||||
"variable": "TUESDAY_OUT",
|
"variable": "TUESDAY_OUT",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Martes · Hora de salida",
|
"question": "Martes · Hora de salida",
|
||||||
"options": ["4:00 PM","5:00 PM","6:00 PM","7:00 PM","8:00 PM"],
|
"options": ["4:00 PM", "5:00 PM", "6:00 PM", "7:00 PM", "8:00 PM"]
|
||||||
"next_step": 6
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 6,
|
"state": 5,
|
||||||
"variable": "WEDNESDAY_IN",
|
"variable": "WEDNESDAY_IN",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Miércoles · Hora de entrada",
|
"question": "Miércoles · Hora de entrada",
|
||||||
"options": ["10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM"],
|
"options": ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM"]
|
||||||
"next_step": 7
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 7,
|
"state": 6,
|
||||||
"variable": "WEDNESDAY_OUT",
|
"variable": "WEDNESDAY_OUT",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Miércoles · Hora de salida",
|
"question": "Miércoles · Hora de salida",
|
||||||
"options": ["4:00 PM","5:00 PM","6:00 PM","7:00 PM","8:00 PM"],
|
"options": ["4:00 PM", "5:00 PM", "6:00 PM", "7:00 PM", "8:00 PM"]
|
||||||
"next_step": 8
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 8,
|
"state": 7,
|
||||||
"variable": "THURSDAY_IN",
|
"variable": "THURSDAY_IN",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Jueves · Hora de entrada",
|
"question": "Jueves · Hora de entrada",
|
||||||
"options": ["10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM"],
|
"options": ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM"]
|
||||||
"next_step": 9
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 9,
|
"state": 8,
|
||||||
"variable": "THURSDAY_OUT",
|
"variable": "THURSDAY_OUT",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Jueves · Hora de salida",
|
"question": "Jueves · Hora de salida",
|
||||||
"options": ["4:00 PM","5:00 PM","6:00 PM","7:00 PM","8:00 PM"],
|
"options": ["4:00 PM", "5:00 PM", "6:00 PM", "7:00 PM", "8:00 PM"]
|
||||||
"next_step": 10
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 10,
|
"state": 9,
|
||||||
"variable": "FRIDAY_IN",
|
"variable": "FRIDAY_IN",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Viernes · Hora de entrada",
|
"question": "Viernes · Hora de entrada",
|
||||||
"options": ["10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM"],
|
"options": ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM"]
|
||||||
"next_step": 11
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 11,
|
"state": 10,
|
||||||
"variable": "FRIDAY_OUT",
|
"variable": "FRIDAY_OUT",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Viernes · Hora de salida",
|
"question": "Viernes · Hora de salida",
|
||||||
"options": ["4:00 PM","5:00 PM","6:00 PM","7:00 PM","8:00 PM"],
|
"options": ["4:00 PM", "5:00 PM", "6:00 PM", "7:00 PM", "8:00 PM"]
|
||||||
"next_step": 12
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 12,
|
"state": 11,
|
||||||
"variable": "SATURDAY_IN",
|
"variable": "SATURDAY_IN",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"question": "Sábado · Hora de entrada",
|
"question": "Sábado · Hora de entrada",
|
||||||
"options": ["10:00 AM","11:00 AM","12:00 PM","1:00 PM","2:00 PM","3:00 PM"],
|
"options": ["10:00 AM", "11:00 AM", "12:00 PM", "1:00 PM", "2:00 PM", "3:00 PM"]
|
||||||
"next_step": 13
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 13,
|
"state": 12,
|
||||||
"variable": "SATURDAY_OUT",
|
"variable": "SATURDAY_OUT",
|
||||||
"type": "info",
|
"type": "info",
|
||||||
"question": "Sábado · Hora de salida\n\nLa salida del sábado queda registrada automáticamente a las 6:00 PM.",
|
"question": "Sábado · Hora de salida\n\nLa salida del sábado queda registrada automáticamente a las 6:00 PM."
|
||||||
"next_step": -1
|
},
|
||||||
|
{
|
||||||
|
"state": 99,
|
||||||
|
"variable": "FLOW_END",
|
||||||
|
"type": "info",
|
||||||
|
"question": "HORARIOS REGISTRADOS\n\nTus horarios quedaron guardados correctamente. Si necesitas un ajuste, notifícalo a administración."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
{
|
{
|
||||||
"flow_name": "leave_request",
|
"flow_name": "leave_request",
|
||||||
"steps": [
|
"steps": [
|
||||||
|
{
|
||||||
|
"state": "INTRO_ALERT",
|
||||||
|
"variable": "AVISO_INICIAL",
|
||||||
|
"question": "⏱️ SOLICITUD DE PERMISO POR HORAS\n\nConfirma que le avisaste a tu manager y que necesitas registrar un permiso formal.",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Continuar"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": "INTRO_SCOPE",
|
||||||
|
"variable": "INTRO_SCOPE",
|
||||||
|
"question": "SECCIÓN ÚNICA · FECHAS, HORARIO Y MOTIVO\n\nResponde exactamente lo que se te pide para evitar rechazos.",
|
||||||
|
"type": "info"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"state": "PERMISO_CUANDO",
|
"state": "PERMISO_CUANDO",
|
||||||
"question": "⏱️ **Solicitud de Permiso**\n\n¿Para cuándo lo necesitas?",
|
"question": "¿Para cuándo lo necesitas?",
|
||||||
"handler": "recibir_cuando_permiso",
|
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Hoy", "Mañana", "Pasado mañana", "Fecha específica"],
|
"options": ["Hoy", "Mañana", "Pasado mañana", "Fecha específica"],
|
||||||
"next_step": [
|
"next_step": [
|
||||||
@@ -21,7 +33,6 @@
|
|||||||
{
|
{
|
||||||
"state": "PERMISO_ANIO",
|
"state": "PERMISO_ANIO",
|
||||||
"question": "¿Para qué año es el permiso? (elige el actual o el siguiente)",
|
"question": "¿Para qué año es el permiso? (elige el actual o el siguiente)",
|
||||||
"handler": "recibir_anio_permiso",
|
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["current_year", "next_year"],
|
"options": ["current_year", "next_year"],
|
||||||
"next_step": "INICIO_DIA"
|
"next_step": "INICIO_DIA"
|
||||||
@@ -29,14 +40,12 @@
|
|||||||
{
|
{
|
||||||
"state": "INICIO_DIA",
|
"state": "INICIO_DIA",
|
||||||
"question": "¿En qué *día* inicia el permiso? (número, ej: 12)",
|
"question": "¿En qué *día* inicia el permiso? (número, ej: 12)",
|
||||||
"handler": "recibir_inicio_dia",
|
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": "INICIO_MES"
|
"next_step": "INICIO_MES"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "INICIO_MES",
|
"state": "INICIO_MES",
|
||||||
"question": "¿De qué *mes* inicia?",
|
"question": "¿De qué *mes* inicia?",
|
||||||
"handler": "recibir_inicio_mes",
|
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": [
|
"options": [
|
||||||
"Enero", "Febrero", "Marzo",
|
"Enero", "Febrero", "Marzo",
|
||||||
@@ -49,14 +58,12 @@
|
|||||||
{
|
{
|
||||||
"state": "FIN_DIA",
|
"state": "FIN_DIA",
|
||||||
"question": "¿Qué *día* termina?",
|
"question": "¿Qué *día* termina?",
|
||||||
"handler": "recibir_fin_dia",
|
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": "FIN_MES"
|
"next_step": "FIN_MES"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "FIN_MES",
|
"state": "FIN_MES",
|
||||||
"question": "¿De qué *mes* termina?",
|
"question": "¿De qué *mes* termina?",
|
||||||
"handler": "recibir_fin_mes",
|
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": [
|
"options": [
|
||||||
"Enero", "Febrero", "Marzo",
|
"Enero", "Febrero", "Marzo",
|
||||||
@@ -69,15 +76,20 @@
|
|||||||
{
|
{
|
||||||
"state": "HORARIO",
|
"state": "HORARIO",
|
||||||
"question": "¿Cuál es el horario? Ej: `09:00-11:00` o `Todo el día`.",
|
"question": "¿Cuál es el horario? Ej: `09:00-11:00` o `Todo el día`.",
|
||||||
"handler": "recibir_horario",
|
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": "MOTIVO"
|
"next_step": "MOTIVO"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "MOTIVO",
|
"state": "MOTIVO",
|
||||||
"question": "Entendido. ¿Cuál es el motivo o comentario adicional?",
|
"question": "Entendido. ¿Cuál es el motivo o comentario adicional?",
|
||||||
"handler": "recibir_motivo_fin",
|
"type": "text",
|
||||||
"type": "text"
|
"next_step": "FLOW_END"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": "FLOW_END",
|
||||||
|
"variable": "FLOW_END",
|
||||||
|
"question": "✅ Gracias. Tu permiso quedó registrado y RH revisará la información. Si necesitas otro, vuelve a iniciar con /permiso.",
|
||||||
|
"type": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,55 +1,67 @@
|
|||||||
{
|
{
|
||||||
"flow_name": "onboarding",
|
"flow_name": "onboarding",
|
||||||
"steps": [
|
"steps": [
|
||||||
|
{
|
||||||
|
"state": -2,
|
||||||
|
"variable": "AVISO_INICIAL",
|
||||||
|
"question": "⚠️ AVISO IMPORTANTE\n\nEste formulario crea tu expediente oficial.\n\n• Lee cada pregunta con atención.\n• Responde solo lo que se te pide.\n• Escribe /cancelar en cualquier momento para detener el proceso.\n\nConfirma cuando estés lista.",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Comenzar"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": -1,
|
||||||
|
"variable": "INTRO_PERSONALES",
|
||||||
|
"question": "SECCIÓN 1 · DATOS PERSONALES\n\nUsaremos tu información exactamente como aparece en tus documentos oficiales.\n\n(Confirma para continuar)",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Continuar"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"state": 0,
|
"state": 0,
|
||||||
"variable": "NOMBRE_SALUDO",
|
"variable": "NOMBRE_SALUDO",
|
||||||
"question": "¿Cómo te gusta que te llamemos?",
|
"question": "¿Cómo te gusta que te llamemos?",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 1,
|
"state": 1,
|
||||||
"variable": "NOMBRE_COMPLETO",
|
"variable": "NOMBRE_COMPLETO",
|
||||||
"question": "Escribe tus nombres (SIN apellidos), exactamente como aparecen en tu INE.",
|
"question": "Escribe tus nombres (SIN apellidos), exactamente como aparecen en tu INE.",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 2
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 2,
|
"state": 2,
|
||||||
"variable": "APELLIDO_PATERNO",
|
"variable": "APELLIDO_PATERNO",
|
||||||
"question": "Apellido paterno:",
|
"question": "Apellido paterno:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 3
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 3,
|
"state": 3,
|
||||||
"variable": "APELLIDO_MATERNO",
|
"variable": "APELLIDO_MATERNO",
|
||||||
"question": "Apellido materno:",
|
"question": "Apellido materno:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 4
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 4,
|
"state": 4,
|
||||||
"variable": "CUMPLE_DIA",
|
"variable": "CUMPLE_DIA",
|
||||||
"question": "Fecha de nacimiento · Día (solo número, ej. 13)",
|
"question": "Fecha de nacimiento · Día (solo número, ej. 13)",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 5
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 5,
|
"state": 5,
|
||||||
"variable": "CUMPLE_MES",
|
"variable": "CUMPLE_MES",
|
||||||
"question": "Fecha de nacimiento · Mes",
|
"question": "Fecha de nacimiento · Mes",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
|
"options": [
|
||||||
"next_step": 6
|
"Enero", "Febrero", "Marzo",
|
||||||
|
"Abril", "Mayo", "Junio",
|
||||||
|
"Julio", "Agosto", "Septiembre",
|
||||||
|
"Octubre", "Noviembre", "Diciembre"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 6,
|
"state": 6,
|
||||||
"variable": "CUMPLE_ANIO",
|
"variable": "CUMPLE_ANIO",
|
||||||
"question": "Fecha de nacimiento · Año (4 dígitos)",
|
"question": "Fecha de nacimiento · Año (4 dígitos)",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 7
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 7,
|
"state": 7,
|
||||||
@@ -59,7 +71,7 @@
|
|||||||
"options": ["Coahuila", "Nuevo León", "Otro"],
|
"options": ["Coahuila", "Nuevo León", "Otro"],
|
||||||
"next_steps": [
|
"next_steps": [
|
||||||
{ "value": "Otro", "go_to": 7.1 },
|
{ "value": "Otro", "go_to": 7.1 },
|
||||||
{ "value": "default", "go_to": 8 }
|
{ "value": "default", "go_to": 7.5 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -67,70 +79,68 @@
|
|||||||
"variable": "ESTADO_NACIMIENTO_OTRO",
|
"variable": "ESTADO_NACIMIENTO_OTRO",
|
||||||
"question": "Escribe el nombre del estado donde naciste.",
|
"question": "Escribe el nombre del estado donde naciste.",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": 8
|
"next_step": 7.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 7.5,
|
||||||
|
"variable": "INTRO_ADMIN",
|
||||||
|
"question": "SECCIÓN 2 · DATOS ADMINISTRATIVOS\n\nEstos datos se usan para contrato, nómina y comunicación oficial.\n\n(Confirma para continuar)",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Continuar"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 8,
|
"state": 8,
|
||||||
"variable": "RFC",
|
"variable": "RFC",
|
||||||
"question": "RFC completo (13 caracteres, sin espacios):",
|
"question": "RFC completo (13 caracteres, sin espacios):",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 9
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 9,
|
"state": 9,
|
||||||
"variable": "CURP",
|
"variable": "CURP",
|
||||||
"question": "CURP completo (18 caracteres):",
|
"question": "CURP completo (18 caracteres):",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 10
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 10,
|
"state": 10,
|
||||||
"variable": "CORREO",
|
"variable": "CORREO",
|
||||||
"question": "Correo electrónico personal:",
|
"question": "Correo electrónico personal:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 11
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 11,
|
"state": 11,
|
||||||
"variable": "CELULAR",
|
"variable": "CELULAR",
|
||||||
"question": "Número de celular (10 dígitos):",
|
"question": "Número de celular (10 dígitos):",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 12
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 12,
|
"state": 12,
|
||||||
"variable": "CALLE",
|
"variable": "CALLE",
|
||||||
"question": "Domicilio · Calle:",
|
"question": "Domicilio · Calle:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 13
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 13,
|
"state": 13,
|
||||||
"variable": "NUM_EXTERIOR",
|
"variable": "NUM_EXTERIOR",
|
||||||
"question": "Domicilio · Número exterior:",
|
"question": "Domicilio · Número exterior:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 14
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 14,
|
"state": 14,
|
||||||
"variable": "NUM_INTERIOR",
|
"variable": "NUM_INTERIOR",
|
||||||
"question": "Domicilio · Número interior (0 si no aplica):",
|
"question": "Domicilio · Número interior (0 si no aplica):",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 15
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 15,
|
"state": 15,
|
||||||
"variable": "COLONIA",
|
"variable": "COLONIA",
|
||||||
"question": "Domicilio · Colonia:",
|
"question": "Domicilio · Colonia:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 16
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 16,
|
"state": 16,
|
||||||
"variable": "CODIGO_POSTAL",
|
"variable": "CODIGO_POSTAL",
|
||||||
"question": "Código Postal (5 dígitos):",
|
"question": "Código Postal (5 dígitos):",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 17
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 17,
|
"state": 17,
|
||||||
@@ -138,7 +148,7 @@
|
|||||||
"question": "Ciudad de residencia:",
|
"question": "Ciudad de residencia:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Saltillo", "Ramos Arizpe", "Arteaga", "Otro"],
|
"options": ["Saltillo", "Ramos Arizpe", "Arteaga", "Otro"],
|
||||||
"next_steps": [
|
"next_steps": [
|
||||||
{ "value": "Otro", "go_to": 17.1 },
|
{ "value": "Otro", "go_to": 17.1 },
|
||||||
{ "value": "default", "go_to": 18 }
|
{ "value": "default", "go_to": 18 }
|
||||||
]
|
]
|
||||||
@@ -155,167 +165,179 @@
|
|||||||
"variable": "ROL",
|
"variable": "ROL",
|
||||||
"question": "Rol dentro del equipo:",
|
"question": "Rol dentro del equipo:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Belleza", "Staff (Recepción)", "Marketing"],
|
"options": ["Belleza", "Staff (Recepción)", "Marketing"]
|
||||||
"next_step": 19
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 19,
|
"state": 19,
|
||||||
"variable": "SUCURSAL",
|
"variable": "SUCURSAL",
|
||||||
"question": "Sucursal principal:",
|
"question": "Sucursal principal:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Plaza Cima (Sur)", "Plaza O (Carranza)"],
|
"options": ["Plaza Cima (Sur)", "Plaza O (Carranza)"]
|
||||||
"next_step": 20
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 20,
|
"state": 20,
|
||||||
"variable": "INICIO_DIA",
|
"variable": "INICIO_DIA",
|
||||||
"question": "Fecha de ingreso · Día:",
|
"question": "Fecha de ingreso · Día:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 21
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 21,
|
"state": 21,
|
||||||
"variable": "INICIO_MES",
|
"variable": "INICIO_MES",
|
||||||
"question": "Fecha de ingreso · Mes:",
|
"question": "Fecha de ingreso · Mes:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
|
"options": [
|
||||||
"next_step": 22
|
"Enero", "Febrero", "Marzo",
|
||||||
|
"Abril", "Mayo", "Junio",
|
||||||
|
"Julio", "Agosto", "Septiembre",
|
||||||
|
"Octubre", "Noviembre", "Diciembre"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 22,
|
"state": 22,
|
||||||
"variable": "INICIO_ANIO",
|
"variable": "INICIO_ANIO",
|
||||||
"question": "Fecha de ingreso · Año:",
|
"question": "Fecha de ingreso · Año:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["2024", "2025", "2026"],
|
"options": ["2024", "2025", "2026"]
|
||||||
"next_step": 23
|
},
|
||||||
|
{
|
||||||
|
"state": 22.5,
|
||||||
|
"variable": "INTRO_EMERGENCIA",
|
||||||
|
"question": "SECCIÓN 3 · CONTACTO DE EMERGENCIA\n\nEsta persona solo será contactada en una emergencia real.\n\n(Confirma para continuar)",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Continuar"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 23,
|
"state": 23,
|
||||||
"variable": "REF1_NOMBRE",
|
"variable": "EMERGENCIA_NOMBRE",
|
||||||
"question": "Referencia 1 · Nombre completo:",
|
"question": "Contacto de emergencia · Nombre completo:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 24
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 24,
|
"state": 24,
|
||||||
"variable": "REF1_TELEFONO",
|
"variable": "EMERGENCIA_TEL",
|
||||||
"question": "Referencia 1 · Teléfono:",
|
"question": "Contacto de emergencia · Teléfono:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 25
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 25,
|
"state": 25,
|
||||||
"variable": "REF1_TIPO",
|
"variable": "EMERGENCIA_RELACION",
|
||||||
"question": "Referencia 1 · Relación:",
|
"question": "Relación con el contacto de emergencia:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"],
|
"options": ["Padre/Madre", "Pareja", "Hermano/a", "Hijo/a", "Amigo/a", "Otro"],
|
||||||
"next_steps": [
|
"next_steps": [
|
||||||
{ "value": "Otra", "go_to": 25.1 },
|
{ "value": "Otro", "go_to": 25.1 },
|
||||||
{ "value": "default", "go_to": 26 }
|
{ "value": "default", "go_to": 26 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 25.1,
|
"state": 25.1,
|
||||||
"variable": "REF1_TIPO_OTRA",
|
"variable": "EMERGENCIA_RELACION_OTRA",
|
||||||
"question": "Especifíca la relación con la Referencia 1:",
|
"question": "Describe la relación con tu contacto de emergencia:",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": 26
|
"next_step": 26
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 26,
|
"state": 26,
|
||||||
"variable": "REF2_NOMBRE",
|
"variable": "INTRO_REFERENCIAS",
|
||||||
"question": "Referencia 2 · Nombre completo:",
|
"question": "SECCIÓN 4 · REFERENCIAS PERSONALES\n\nNecesitamos 3 referencias reales que te conozcan.\n\n(Confirma para continuar)",
|
||||||
"type": "text",
|
"type": "keyboard",
|
||||||
"next_step": 27
|
"options": ["Continuar"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 27,
|
"state": 27,
|
||||||
"variable": "REF2_TELEFONO",
|
"variable": "REF1_NOMBRE",
|
||||||
"question": "Referencia 2 · Teléfono:",
|
"question": "Referencia 1 · Nombre completo:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 28
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 28,
|
"state": 28,
|
||||||
"variable": "REF2_TIPO",
|
"variable": "REF1_TELEFONO",
|
||||||
"question": "Referencia 2 · Relación:",
|
"question": "Referencia 1 · Teléfono:",
|
||||||
"type": "keyboard",
|
"type": "text"
|
||||||
"options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"],
|
|
||||||
"next_steps": [
|
|
||||||
{ "value": "Otra", "go_to": 28.1 },
|
|
||||||
{ "value": "default", "go_to": 29 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"state": 28.1,
|
|
||||||
"variable": "REF2_TIPO_OTRA",
|
|
||||||
"question": "Especifíca la relación con la Referencia 2:",
|
|
||||||
"type": "text",
|
|
||||||
"next_step": 29
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 29,
|
"state": 29,
|
||||||
"variable": "REF3_NOMBRE",
|
"variable": "REF1_TIPO",
|
||||||
"question": "Referencia 3 · Nombre completo:",
|
"question": "Referencia 1 · Relación:",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"],
|
||||||
|
"next_steps": [
|
||||||
|
{ "value": "Otra", "go_to": 29.1 },
|
||||||
|
{ "value": "default", "go_to": 30 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 29.1,
|
||||||
|
"variable": "REF1_TIPO_OTRA",
|
||||||
|
"question": "Especifica la relación con la Referencia 1:",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": 30
|
"next_step": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 30,
|
"state": 30,
|
||||||
"variable": "REF3_TELEFONO",
|
"variable": "REF2_NOMBRE",
|
||||||
"question": "Referencia 3 · Teléfono:",
|
"question": "Referencia 2 · Nombre completo:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 31
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 31,
|
"state": 31,
|
||||||
"variable": "REF3_TIPO",
|
"variable": "REF2_TELEFONO",
|
||||||
"question": "Referencia 3 · Relación:",
|
"question": "Referencia 2 · Teléfono:",
|
||||||
"type": "keyboard",
|
"type": "text"
|
||||||
"options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"],
|
|
||||||
"next_steps": [
|
|
||||||
{ "value": "Otra", "go_to": 31.1 },
|
|
||||||
{ "value": "default", "go_to": 32 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"state": 31.1,
|
|
||||||
"variable": "REF3_TIPO_OTRA",
|
|
||||||
"question": "Especifíca la relación con la Referencia 3:",
|
|
||||||
"type": "text",
|
|
||||||
"next_step": 32
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 32,
|
"state": 32,
|
||||||
"variable": "EMERGENCIA_NOMBRE",
|
"variable": "REF2_TIPO",
|
||||||
"question": "Contacto de emergencia · Nombre completo:",
|
"question": "Referencia 2 · Relación:",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"],
|
||||||
|
"next_steps": [
|
||||||
|
{ "value": "Otra", "go_to": 32.1 },
|
||||||
|
{ "value": "default", "go_to": 33 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 32.1,
|
||||||
|
"variable": "REF2_TIPO_OTRA",
|
||||||
|
"question": "Especifica la relación con la Referencia 2:",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": 33
|
"next_step": 33
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 33,
|
"state": 33,
|
||||||
"variable": "EMERGENCIA_TEL",
|
"variable": "REF3_NOMBRE",
|
||||||
"question": "Contacto de emergencia · Teléfono:",
|
"question": "Referencia 3 · Nombre completo:",
|
||||||
"type": "text",
|
"type": "text"
|
||||||
"next_step": 34
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 34,
|
"state": 34,
|
||||||
"variable": "EMERGENCIA_RELACION",
|
"variable": "REF3_TELEFONO",
|
||||||
"question": "Relación con el contacto de emergencia:",
|
"question": "Referencia 3 · Teléfono:",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 35,
|
||||||
|
"variable": "REF3_TIPO",
|
||||||
|
"question": "Referencia 3 · Relación:",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["Padre/Madre", "Esposo/a", "Pareja", "Hijo/a", "Hermano/a", "Amigo/a", "Otro"],
|
"options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"],
|
||||||
"next_steps": [
|
"next_steps": [
|
||||||
{ "value": "Otro", "go_to": 34.1 },
|
{ "value": "Otra", "go_to": 35.1 },
|
||||||
{ "value": "default", "go_to": -1 }
|
{ "value": "default", "go_to": 99 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": 34.1,
|
"state": 35.1,
|
||||||
"variable": "EMERGENCIA_RELACION_OTRA",
|
"variable": "REF3_TIPO_OTRA",
|
||||||
"question": "Especifíca la relación con el contacto de emergencia:",
|
"question": "Especifica la relación con la Referencia 3:",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"next_step": -1
|
"next_step": 99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 99,
|
||||||
|
"variable": "FLOW_END",
|
||||||
|
"question": "✅ ¡Gracias!\n\nTu registro quedó completo. RH validará la información y te confirmará los siguientes pasos.\n\nSi necesitas corregir algo, escribe /cancelar y empieza de nuevo.",
|
||||||
|
"type": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,28 @@
|
|||||||
"flow_name": "vacations",
|
"flow_name": "vacations",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"state": "INICIO_DIA",
|
"state": -2,
|
||||||
"question": "🌴 **Solicitud de Vacaciones**\n\nVamos a registrar tu descanso. ¿Qué *día* inicia? (número, ej: 10)",
|
"variable": "AVISO_GENERAL",
|
||||||
"handler": "recibir_inicio_dia",
|
"question": "🌴 SOLICITUD DE VACACIONES\n\nAntes de registrar tus fechas confirma que:\n\n• Ya hablaste con tu manager\n• Las fechas no se traslapan con guardias críticas\n• Sabes que el bot notificará al equipo de RH\n\nConfirma para continuar.",
|
||||||
|
"type": "keyboard",
|
||||||
|
"options": ["Continuar"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": -1,
|
||||||
|
"variable": "INTRO_FECHAS",
|
||||||
|
"question": "SECCIÓN ÚNICA · FECHAS Y MOTIVO\n\nComparte las fechas exactas del descanso y cualquier comentario relevante.",
|
||||||
|
"type": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 0,
|
||||||
|
"variable": "INICIO_DIA",
|
||||||
|
"question": "¿Qué *día* inicia tu descanso? (número, ej: 10)",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "INICIO_MES",
|
"state": 1,
|
||||||
|
"variable": "INICIO_MES",
|
||||||
"question": "¿De qué *mes* inicia?",
|
"question": "¿De qué *mes* inicia?",
|
||||||
"handler": "recibir_inicio_mes",
|
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": [
|
"options": [
|
||||||
"Enero", "Febrero", "Marzo",
|
"Enero", "Febrero", "Marzo",
|
||||||
@@ -20,22 +33,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "INICIO_ANIO",
|
"state": 2,
|
||||||
"question": "¿De qué *año* inicia?",
|
"variable": "INICIO_ANIO",
|
||||||
"handler": "recibir_inicio_anio",
|
"question": "¿De qué *año* inicia? (elige el actual o el siguiente)",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["current_year", "next_year"]
|
"options": ["current_year", "next_year"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "FIN_DIA",
|
"state": 3,
|
||||||
|
"variable": "FIN_DIA",
|
||||||
"question": "¿Qué *día* termina tu descanso?",
|
"question": "¿Qué *día* termina tu descanso?",
|
||||||
"handler": "recibir_fin_dia",
|
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "FIN_MES",
|
"state": 4,
|
||||||
|
"variable": "FIN_MES",
|
||||||
"question": "¿De qué *mes* termina?",
|
"question": "¿De qué *mes* termina?",
|
||||||
"handler": "recibir_fin_mes",
|
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": [
|
"options": [
|
||||||
"Enero", "Febrero", "Marzo",
|
"Enero", "Febrero", "Marzo",
|
||||||
@@ -45,17 +58,23 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "FIN_ANIO",
|
"state": 5,
|
||||||
"question": "¿De qué *año* termina tu descanso?",
|
"variable": "FIN_ANIO",
|
||||||
"handler": "recibir_fin_anio",
|
"question": "¿De qué *año* termina tu descanso? (elige el actual o el siguiente)",
|
||||||
"type": "keyboard",
|
"type": "keyboard",
|
||||||
"options": ["current_year", "next_year"]
|
"options": ["current_year", "next_year"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"state": "MOTIVO",
|
"state": 6,
|
||||||
|
"variable": "MOTIVO",
|
||||||
"question": "Entendido. ¿Cuál es el motivo o comentario adicional?",
|
"question": "Entendido. ¿Cuál es el motivo o comentario adicional?",
|
||||||
"handler": "recibir_motivo_fin",
|
|
||||||
"type": "text"
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": 99,
|
||||||
|
"variable": "FLOW_END",
|
||||||
|
"question": "✅ Solicitud registrada.\n\nRH validará la información y te confirmará si las fechas quedan asignadas. Si necesitas cambiar algo, vuelve a iniciar con /vacaciones.",
|
||||||
|
"type": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,16 @@ CREATE TABLE IF NOT EXISTS permisos (
|
|||||||
FOREIGN KEY (numero_empleado) REFERENCES data_empleadas(numero_empleado)
|
FOREIGN KEY (numero_empleado) REFERENCES data_empleadas(numero_empleado)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS horario_empleadas (
|
||||||
|
id_horario INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
numero_empleado VARCHAR(15),
|
||||||
|
telegram_id BIGINT,
|
||||||
|
dia_semana VARCHAR(20),
|
||||||
|
hora_entrada_teorica TIME,
|
||||||
|
hora_salida_teorica TIME,
|
||||||
|
FOREIGN KEY (numero_empleado) REFERENCES data_empleadas(numero_empleado)
|
||||||
|
);
|
||||||
|
|
||||||
USE vanity_attendance;
|
USE vanity_attendance;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS asistencia_registros (
|
CREATE TABLE IF NOT EXISTS asistencia_registros (
|
||||||
@@ -120,32 +130,3 @@ CREATE TABLE IF NOT EXISTS asistencia_registros (
|
|||||||
telegram_id_usado BIGINT,
|
telegram_id_usado BIGINT,
|
||||||
FOREIGN KEY (numero_empleado) REFERENCES vanity_hr.data_empleadas(numero_empleado)
|
FOREIGN KEY (numero_empleado) REFERENCES vanity_hr.data_empleadas(numero_empleado)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS horario_empleadas (
|
|
||||||
id_horario INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
numero_empleado VARCHAR(15),
|
|
||||||
telegram_id BIGINT,
|
|
||||||
dia_semana VARCHAR(20),
|
|
||||||
hora_entrada_teorica TIME,
|
|
||||||
hora_salida_teorica TIME,
|
|
||||||
FOREIGN KEY (numero_empleado) REFERENCES vanity_hr.data_empleadas(numero_empleado)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS horarios_configurados (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
telegram_id BIGINT NOT NULL,
|
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
short_name VARCHAR(100),
|
|
||||||
monday_in TIME,
|
|
||||||
monday_out TIME,
|
|
||||||
tuesday_in TIME,
|
|
||||||
tuesday_out TIME,
|
|
||||||
wednesday_in TIME,
|
|
||||||
wednesday_out TIME,
|
|
||||||
thursday_in TIME,
|
|
||||||
thursday_out TIME,
|
|
||||||
friday_in TIME,
|
|
||||||
friday_out TIME,
|
|
||||||
saturday_in TIME,
|
|
||||||
saturday_out TIME
|
|
||||||
);
|
|
||||||
|
|||||||
32
db_logic.md
32
db_logic.md
@@ -108,6 +108,20 @@ Tabla central de Recursos Humanos. Contiene información contractual, personal,
|
|||||||
| con_goce_sueldo | tinyint(1) | | 0 / 1 |
|
| con_goce_sueldo | tinyint(1) | | 0 / 1 |
|
||||||
| afecta_nomina | tinyint(1) | | Impacto |
|
| afecta_nomina | tinyint(1) | | Impacto |
|
||||||
|
|
||||||
|
|
||||||
|
#### Tabla: `horario_empleadas` (Diccionario de turnos)
|
||||||
|
|
||||||
|
| Campo | Tipo | Key | Descripción |
|
||||||
|
| -------------------- | ----------- | --- | ---------------- |
|
||||||
|
| id_horario | int | PRI | ID |
|
||||||
|
| numero_empleado | varchar(15) | MUL | Relación RH |
|
||||||
|
| telegram_id | bigint | | Llave webhook |
|
||||||
|
| dia_semana | varchar(20) | | monday, tuesday… |
|
||||||
|
| hora_entrada_teorica | time | | Entrada |
|
||||||
|
| hora_salida_teorica | time | | Salida |
|
||||||
|
|
||||||
|
*Los capturados desde `/horario` generan un registro por día mediante upsert (por `telegram_id` + `dia_semana`).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.2 Base de Datos: `vanity_attendance`
|
### 2.2 Base de Datos: `vanity_attendance`
|
||||||
@@ -128,19 +142,6 @@ Tabla central de Recursos Humanos. Contiene información contractual, personal,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Tabla: `horario_empleadas` (Diccionario de turnos)
|
|
||||||
|
|
||||||
| Campo | Tipo | Key | Descripción |
|
|
||||||
| -------------------- | ----------- | --- | ---------------- |
|
|
||||||
| id_horario | int | PRI | ID |
|
|
||||||
| numero_empleado | varchar(15) | MUL | Relación RH |
|
|
||||||
| telegram_id | bigint | | Llave webhook |
|
|
||||||
| dia_semana | varchar(20) | | monday, tuesday… |
|
|
||||||
| hora_entrada_teorica | time | | Entrada |
|
|
||||||
| hora_salida_teorica | time | | Salida |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.3 Base de Datos: `USERS_ALMA`
|
### 2.3 Base de Datos: `USERS_ALMA`
|
||||||
|
|
||||||
#### Tabla: `users` (10 campos)
|
#### Tabla: `users` (10 campos)
|
||||||
@@ -171,7 +172,7 @@ Tabla central de Recursos Humanos. Contiene información contractual, personal,
|
|||||||
### 3.2 Asistencia
|
### 3.2 Asistencia
|
||||||
|
|
||||||
* Identificación por `telegram_id`
|
* Identificación por `telegram_id`
|
||||||
* Cruce con `horario_empleadas` según día
|
* Cruce con `vanity_hr.horario_empleadas` según día
|
||||||
* Cálculo de retraso contra horario teórico
|
* Cálculo de retraso contra horario teórico
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -180,6 +181,7 @@ Tabla central de Recursos Humanos. Contiene información contractual, personal,
|
|||||||
|
|
||||||
* **Identificación**: `body.telegram.user_id`
|
* **Identificación**: `body.telegram.user_id`
|
||||||
* **Operación**: Upsert por día
|
* **Operación**: Upsert por día
|
||||||
|
* **Persistencia**: `vanity_hr.horario_empleadas` (clave `telegram_id` + `dia_semana`)
|
||||||
* **Formato**: conversión de `10:00 AM` → `10:00:00`
|
* **Formato**: conversión de `10:00 AM` → `10:00:00`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -199,7 +201,7 @@ LIMIT 1;
|
|||||||
|
|
||||||
-- Horario del día
|
-- Horario del día
|
||||||
SELECT hora_entrada_teorica
|
SELECT hora_entrada_teorica
|
||||||
FROM vanity_attendance.horario_empleadas
|
FROM vanity_hr.horario_empleadas
|
||||||
WHERE telegram_id = ? AND dia_semana = 'monday';
|
WHERE telegram_id = ? AND dia_semana = 'monday';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
6
main.py
6
main.py
@@ -20,6 +20,8 @@ from modules.flow_builder import load_flows
|
|||||||
from modules.logger import log_request
|
from modules.logger import log_request
|
||||||
from modules.database import chat_id_exists # Importar chat_id_exists
|
from modules.database import chat_id_exists # Importar chat_id_exists
|
||||||
from modules.ui import main_actions_keyboard
|
from modules.ui import main_actions_keyboard
|
||||||
|
from modules.onboarding import onboarding_handler
|
||||||
|
from modules.rh_requests import vacaciones_handler, permiso_handler
|
||||||
# from modules.finder import finder_handler (Si lo creas después)
|
# from modules.finder import finder_handler (Si lo creas después)
|
||||||
|
|
||||||
# Cargar links desde variables de entorno
|
# Cargar links desde variables de entorno
|
||||||
@@ -118,6 +120,10 @@ def main():
|
|||||||
for handler in flow_handlers:
|
for handler in flow_handlers:
|
||||||
app.add_handler(handler)
|
app.add_handler(handler)
|
||||||
|
|
||||||
|
app.add_handler(onboarding_handler)
|
||||||
|
app.add_handler(vacaciones_handler)
|
||||||
|
app.add_handler(permiso_handler)
|
||||||
|
|
||||||
app.add_handler(CommandHandler("links", links_menu))
|
app.add_handler(CommandHandler("links", links_menu))
|
||||||
# app.add_handler(finder_handler)
|
# app.add_handler(finder_handler)
|
||||||
|
|
||||||
|
|||||||
@@ -18,36 +18,3 @@ class AsistenciaRegistros(Base):
|
|||||||
sucursal_registro = Column(String(50))
|
sucursal_registro = Column(String(50))
|
||||||
telegram_id_usado = Column(BigInteger)
|
telegram_id_usado = Column(BigInteger)
|
||||||
empleada = relationship("DataEmpleadas", backref="asistencia_registros")
|
empleada = relationship("DataEmpleadas", backref="asistencia_registros")
|
||||||
|
|
||||||
class HorarioEmpleadas(Base):
|
|
||||||
__tablename__ = 'horario_empleadas'
|
|
||||||
__table_args__ = {'schema': 'vanity_attendance'}
|
|
||||||
|
|
||||||
id_horario = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
numero_empleado = Column(String(15), ForeignKey('vanity_hr.data_empleadas.numero_empleado'))
|
|
||||||
telegram_id = Column(BigInteger)
|
|
||||||
dia_semana = Column(String(20))
|
|
||||||
hora_entrada_teorica = Column(Time)
|
|
||||||
hora_salida_teorica = Column(Time)
|
|
||||||
empleada = relationship("DataEmpleadas", backref="horario_empleadas")
|
|
||||||
|
|
||||||
class HorariosConfigurados(Base):
|
|
||||||
__tablename__ = 'horarios_configurados'
|
|
||||||
__table_args__ = {'schema': 'vanity_attendance'}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
telegram_id = Column(BigInteger, nullable=False)
|
|
||||||
timestamp = Column(Time)
|
|
||||||
short_name = Column(String(100))
|
|
||||||
monday_in = Column(Time)
|
|
||||||
monday_out = Column(Time)
|
|
||||||
tuesday_in = Column(Time)
|
|
||||||
tuesday_out = Column(Time)
|
|
||||||
wednesday_in = Column(Time)
|
|
||||||
wednesday_out = Column(Time)
|
|
||||||
thursday_in = Column(Time)
|
|
||||||
thursday_out = Column(Time)
|
|
||||||
friday_in = Column(Time)
|
|
||||||
friday_out = Column(Time)
|
|
||||||
saturday_in = Column(Time)
|
|
||||||
saturday_out = Column(Time)
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import create_engine, Column, Integer, String, Enum, TIMESTAMP, Date, Text, BigInteger, DateTime
|
from sqlalchemy import create_engine, Column, Integer, String, Enum, TIMESTAMP, Date, Text, BigInteger, DateTime, Time
|
||||||
from sqlalchemy.dialects.mysql import TINYINT
|
from sqlalchemy.dialects.mysql import TINYINT
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
@@ -90,3 +90,15 @@ class Permisos(Base):
|
|||||||
con_goce_sueldo = Column(TINYINT)
|
con_goce_sueldo = Column(TINYINT)
|
||||||
afecta_nomina = Column(TINYINT)
|
afecta_nomina = Column(TINYINT)
|
||||||
empleada = relationship("DataEmpleadas")
|
empleada = relationship("DataEmpleadas")
|
||||||
|
|
||||||
|
class HorarioEmpleadas(Base):
|
||||||
|
__tablename__ = 'horario_empleadas'
|
||||||
|
__table_args__ = {'schema': 'vanity_hr'}
|
||||||
|
|
||||||
|
id_horario = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
numero_empleado = Column(String(15), ForeignKey('vanity_hr.data_empleadas.numero_empleado'))
|
||||||
|
telegram_id = Column(BigInteger)
|
||||||
|
dia_semana = Column(String(20))
|
||||||
|
hora_entrada_teorica = Column(Time)
|
||||||
|
hora_salida_teorica = Column(Time)
|
||||||
|
empleada = relationship("DataEmpleadas", backref="horarios_registrados")
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from datetime import datetime, date
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from models.users_alma_models import Base as BaseUsersAlma, User
|
from models.users_alma_models import Base as BaseUsersAlma, User
|
||||||
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos
|
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos, HorarioEmpleadas
|
||||||
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas
|
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros
|
||||||
|
|
||||||
|
|
||||||
# --- DATABASE (MySQL) SETUP ---
|
# --- DATABASE (MySQL) SETUP ---
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import os
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime, time as time_cls
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from modules.database import get_engine
|
from modules.database import SessionVanityHr
|
||||||
from models.vanity_attendance_models import HorariosConfigurados
|
from models.vanity_hr_models import HorarioEmpleadas, DataEmpleadas
|
||||||
|
|
||||||
def _send_webhook(url: str, payload: dict):
|
def _send_webhook(url: str, payload: dict):
|
||||||
"""Sends a POST request to a webhook."""
|
"""Sends a POST request to a webhook."""
|
||||||
@@ -39,50 +39,87 @@ def _finalize_horario(telegram_id: int, data: dict):
|
|||||||
"""Finalizes the 'horario' flow."""
|
"""Finalizes the 'horario' flow."""
|
||||||
logging.info(f"Finalizing 'horario' flow for telegram_id: {telegram_id}")
|
logging.info(f"Finalizing 'horario' flow for telegram_id: {telegram_id}")
|
||||||
|
|
||||||
# 1. Prepare data
|
# 1. Prepare data for webhook and DB
|
||||||
|
day_pairs = [
|
||||||
|
("monday", "MONDAY_IN", "MONDAY_OUT"),
|
||||||
|
("tuesday", "TUESDAY_IN", "TUESDAY_OUT"),
|
||||||
|
("wednesday", "WEDNESDAY_IN", "WEDNESDAY_OUT"),
|
||||||
|
("thursday", "THURSDAY_IN", "THURSDAY_OUT"),
|
||||||
|
("friday", "FRIDAY_IN", "FRIDAY_OUT"),
|
||||||
|
("saturday", "SATURDAY_IN", None),
|
||||||
|
]
|
||||||
|
|
||||||
schedule_data = {
|
schedule_data = {
|
||||||
"telegram_id": telegram_id,
|
"telegram_id": telegram_id,
|
||||||
"short_name": data.get("SHORT_NAME"),
|
"short_name": data.get("SHORT_NAME"),
|
||||||
"monday_in": _convert_to_time(data.get("MONDAY_IN")),
|
|
||||||
"monday_out": _convert_to_time(data.get("MONDAY_OUT")),
|
|
||||||
"tuesday_in": _convert_to_time(data.get("TUESDAY_IN")),
|
|
||||||
"tuesday_out": _convert_to_time(data.get("TUESDAY_OUT")),
|
|
||||||
"wednesday_in": _convert_to_time(data.get("WEDNESDAY_IN")),
|
|
||||||
"wednesday_out": _convert_to_time(data.get("WEDNESDAY_OUT")),
|
|
||||||
"thursday_in": _convert_to_time(data.get("THURSDAY_IN")),
|
|
||||||
"thursday_out": _convert_to_time(data.get("THURSDAY_OUT")),
|
|
||||||
"friday_in": _convert_to_time(data.get("FRIDAY_IN")),
|
|
||||||
"friday_out": _convert_to_time(data.get("FRIDAY_OUT")),
|
|
||||||
"saturday_in": _convert_to_time(data.get("SATURDAY_IN")),
|
|
||||||
"saturday_out": _convert_to_time("6:00 PM"), # Hardcoded as per flow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rows_for_db = []
|
||||||
|
for day_key, in_key, out_key in day_pairs:
|
||||||
|
entrada = _convert_to_time(data.get(in_key))
|
||||||
|
salida_raw = data.get(out_key) if out_key else "6:00 PM"
|
||||||
|
salida = _convert_to_time(salida_raw)
|
||||||
|
|
||||||
|
schedule_data[f"{day_key}_in"] = entrada
|
||||||
|
schedule_data[f"{day_key}_out"] = salida
|
||||||
|
|
||||||
|
if not entrada or not salida:
|
||||||
|
logging.warning(f"Missing schedule data for {day_key}. Entrada: {entrada}, Salida: {salida}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
rows_for_db.append(
|
||||||
|
{
|
||||||
|
"dia_semana": day_key,
|
||||||
|
"hora_entrada": entrada,
|
||||||
|
"hora_salida": salida,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 2. Send to webhook
|
# 2. Send to webhook
|
||||||
webhook_url = os.getenv("WEBHOOK_SCHEDULE")
|
webhook_url = os.getenv("WEBHOOK_SCHEDULE")
|
||||||
if webhook_url:
|
if webhook_url:
|
||||||
# Create a JSON-serializable payload
|
json_payload = {
|
||||||
json_payload = {k: (v.isoformat() if isinstance(v, datetime.time) else v) for k, v in schedule_data.items()}
|
k: (v.isoformat() if isinstance(v, time_cls) else v) for k, v in schedule_data.items()
|
||||||
|
}
|
||||||
json_payload["timestamp"] = datetime.now().isoformat()
|
json_payload["timestamp"] = datetime.now().isoformat()
|
||||||
_send_webhook(webhook_url, json_payload)
|
_send_webhook(webhook_url, json_payload)
|
||||||
|
|
||||||
# 3. Save to database
|
# 3. Save to database (vanity_hr.horario_empleadas)
|
||||||
engine = get_engine()
|
if not SessionVanityHr:
|
||||||
Session = sessionmaker(bind=engine)
|
logging.error("SessionVanityHr is not initialized. Cannot persist horarios.")
|
||||||
session = Session()
|
return False
|
||||||
|
|
||||||
|
session = SessionVanityHr()
|
||||||
try:
|
try:
|
||||||
# Upsert logic: Check if a record for this telegram_id already exists
|
empleada = session.query(DataEmpleadas).filter(DataEmpleadas.telegram_chat_id == telegram_id).first()
|
||||||
existing_schedule = session.query(HorariosConfigurados).filter_by(telegram_id=telegram_id).first()
|
numero_empleado = empleada.numero_empleado if empleada else None
|
||||||
if existing_schedule:
|
if not numero_empleado:
|
||||||
# Update existing record
|
logging.warning(f"No se encontró numero_empleado para telegram_id={telegram_id}. Se guardará NULL.")
|
||||||
for key, value in schedule_data.items():
|
|
||||||
setattr(existing_schedule, key, value)
|
existing_rows = {
|
||||||
existing_schedule.timestamp = datetime.now()
|
row.dia_semana: row
|
||||||
logging.info(f"Updating existing schedule for telegram_id: {telegram_id}")
|
for row in session.query(HorarioEmpleadas).filter_by(telegram_id=telegram_id).all()
|
||||||
else:
|
}
|
||||||
# Create new record
|
|
||||||
new_schedule = HorariosConfigurados(**schedule_data)
|
for row in rows_for_db:
|
||||||
session.add(new_schedule)
|
dia = row["dia_semana"]
|
||||||
logging.info(f"Creating new schedule for telegram_id: {telegram_id}")
|
entrada = row["hora_entrada"]
|
||||||
|
salida = row["hora_salida"]
|
||||||
|
existing = existing_rows.get(dia)
|
||||||
|
if existing:
|
||||||
|
existing.numero_empleado = numero_empleado or existing.numero_empleado
|
||||||
|
existing.hora_entrada_teorica = entrada
|
||||||
|
existing.hora_salida_teorica = salida
|
||||||
|
else:
|
||||||
|
session.add(
|
||||||
|
HorarioEmpleadas(
|
||||||
|
numero_empleado=numero_empleado,
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
dia_semana=dia,
|
||||||
|
hora_entrada_teorica=entrada,
|
||||||
|
hora_salida_teorica=salida,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,105 +1,153 @@
|
|||||||
|
import ast
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from telegram import Update, ReplyKeyboardRemove, ReplyKeyboardMarkup
|
|
||||||
|
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
|
CommandHandler,
|
||||||
ContextTypes,
|
ContextTypes,
|
||||||
ConversationHandler,
|
ConversationHandler,
|
||||||
MessageHandler,
|
MessageHandler,
|
||||||
CommandHandler,
|
|
||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .finalizer import finalize_flow
|
from .finalizer import finalize_flow
|
||||||
|
|
||||||
async def end_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
||||||
await update.message.reply_text("Flujo cancelado.")
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
async def generic_callback(update: Update, context: ContextTypes.DEFAULT_TYPE, flow: dict):
|
def _build_keyboard(options):
|
||||||
current_state_key = context.user_data.get("current_state", 0)
|
keyboard = [options[i : i + 2] for i in range(0, len(options), 2)]
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
current_step = next((step for step in flow["steps"] if step["state"] == current_state_key), None)
|
keyboard,
|
||||||
|
one_time_keyboard=True,
|
||||||
if not current_step:
|
resize_keyboard=True,
|
||||||
await update.message.reply_text("Hubo un error en el flujo. Por favor, inicia de nuevo.")
|
)
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
user_answer = update.message.text
|
|
||||||
variable_name = current_step.get("variable")
|
|
||||||
if variable_name:
|
|
||||||
context.user_data[variable_name] = user_answer
|
|
||||||
|
|
||||||
next_state_key = None
|
|
||||||
if "next_steps" in current_step:
|
|
||||||
for condition in current_step["next_steps"]:
|
|
||||||
if condition.get("value") == user_answer:
|
|
||||||
next_state_key = condition["go_to"]
|
|
||||||
break
|
|
||||||
elif condition.get("value") == "default":
|
|
||||||
next_state_key = condition["go_to"]
|
|
||||||
elif "next_step" in current_step:
|
|
||||||
next_state_key = current_step["next_step"]
|
|
||||||
|
|
||||||
if next_state_key is None:
|
|
||||||
return await end_cancel(update, context)
|
|
||||||
|
|
||||||
if next_state_key == -1:
|
|
||||||
await finalize_flow(update, context)
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
next_step = next((step for step in flow["steps"] if step["state"] == next_state_key), None)
|
|
||||||
|
|
||||||
if not next_step:
|
|
||||||
await update.message.reply_text("Error: No se encontró el siguiente paso del flujo.")
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
reply_markup = ReplyKeyboardRemove()
|
|
||||||
if next_step.get("type") == "keyboard" and "options" in next_step:
|
|
||||||
# Create a 2D array for the keyboard
|
|
||||||
options = next_step["options"]
|
|
||||||
keyboard = [options[i:i+2] for i in range(0, len(options), 2)]
|
|
||||||
reply_markup = ReplyKeyboardMarkup(
|
|
||||||
keyboard,
|
|
||||||
one_time_keyboard=True, resize_keyboard=True
|
|
||||||
)
|
|
||||||
|
|
||||||
await update.message.reply_text(next_step["question"], reply_markup=reply_markup)
|
|
||||||
|
|
||||||
context.user_data["current_state"] = next_state_key
|
|
||||||
return next_state_key
|
|
||||||
|
|
||||||
|
|
||||||
async def start_flow(update: Update, context: ContextTypes.DEFAULT_TYPE, flow: dict):
|
def _preprocess_flow(flow: dict):
|
||||||
context.user_data.clear()
|
"""Populate missing next_step values assuming a linear order."""
|
||||||
context.user_data["flow_name"] = flow["flow_name"]
|
steps = flow.get("steps", [])
|
||||||
|
for idx, step in enumerate(steps):
|
||||||
|
if "next_step" in step or "next_steps" in step:
|
||||||
|
continue
|
||||||
|
if idx + 1 < len(steps):
|
||||||
|
step["next_step"] = steps[idx + 1]["state"]
|
||||||
|
else:
|
||||||
|
step["next_step"] = -1
|
||||||
|
|
||||||
first_step = flow["steps"][0]
|
|
||||||
context.user_data["current_state"] = first_step["state"]
|
|
||||||
|
|
||||||
reply_markup = ReplyKeyboardRemove()
|
def _find_step(flow: dict, state_key):
|
||||||
if first_step.get("type") == "keyboard" and "options" in first_step:
|
return next((step for step in flow["steps"] if step["state"] == state_key), None)
|
||||||
options = first_step["options"]
|
|
||||||
keyboard = [options[i:i+2] for i in range(0, len(options), 2)]
|
|
||||||
reply_markup = ReplyKeyboardMarkup(
|
|
||||||
keyboard,
|
|
||||||
one_time_keyboard=True, resize_keyboard=True
|
|
||||||
)
|
|
||||||
|
|
||||||
await update.message.reply_text(first_step["question"], reply_markup=reply_markup)
|
|
||||||
return first_step["state"]
|
ALLOWED_AST_NODES = (
|
||||||
|
ast.Expression,
|
||||||
|
ast.BoolOp,
|
||||||
|
ast.Compare,
|
||||||
|
ast.Name,
|
||||||
|
ast.Load,
|
||||||
|
ast.Constant,
|
||||||
|
ast.List,
|
||||||
|
ast.Tuple,
|
||||||
|
ast.And,
|
||||||
|
ast.Or,
|
||||||
|
ast.Eq,
|
||||||
|
ast.NotEq,
|
||||||
|
ast.In,
|
||||||
|
ast.NotIn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _evaluate_condition(condition: str, response: str) -> bool:
|
||||||
|
"""Safely evaluate expressions like `response in ['Hoy', 'Mañana']`."""
|
||||||
|
if not condition:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
tree = ast.parse(condition, mode="eval")
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ALLOWED_AST_NODES):
|
||||||
|
raise ValueError(f"Unsupported expression: {condition}")
|
||||||
|
compiled = compile(tree, "<condition>", "eval")
|
||||||
|
return bool(eval(compiled, {"__builtins__": {}}, {"response": response}))
|
||||||
|
except Exception as exc:
|
||||||
|
logging.warning("Failed to evaluate condition '%s': %s", condition, exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _determine_next_state(step: dict, user_answer: str):
|
||||||
|
"""Resolve the next state declared in the JSON step."""
|
||||||
|
if "next_steps" in step:
|
||||||
|
default_target = None
|
||||||
|
for option in step["next_steps"]:
|
||||||
|
value = option.get("value")
|
||||||
|
if value == "default":
|
||||||
|
default_target = option.get("go_to")
|
||||||
|
elif user_answer == value:
|
||||||
|
return option.get("go_to")
|
||||||
|
return default_target
|
||||||
|
|
||||||
|
next_step = step.get("next_step")
|
||||||
|
|
||||||
|
if isinstance(next_step, list):
|
||||||
|
default_target = None
|
||||||
|
for option in next_step:
|
||||||
|
condition = option.get("condition")
|
||||||
|
target = option.get("state")
|
||||||
|
if condition:
|
||||||
|
if _evaluate_condition(condition, user_answer):
|
||||||
|
return target
|
||||||
|
elif option.get("value") and user_answer == option["value"]:
|
||||||
|
return target
|
||||||
|
elif option.get("default"):
|
||||||
|
default_target = target
|
||||||
|
return default_target
|
||||||
|
|
||||||
|
return next_step
|
||||||
|
|
||||||
|
|
||||||
|
async def _go_to_state(update: Update, context: ContextTypes.DEFAULT_TYPE, flow: dict, state_key):
|
||||||
|
"""Send the question for the requested state, skipping info-only steps."""
|
||||||
|
safety_counter = 0
|
||||||
|
while True:
|
||||||
|
safety_counter += 1
|
||||||
|
if safety_counter > len(flow["steps"]) + 2:
|
||||||
|
logging.error("Detected potential loop while traversing flow '%s'", flow.get("flow_name"))
|
||||||
|
await update.message.reply_text("Ocurrió un error al continuar con el flujo. Intenta iniciar de nuevo.")
|
||||||
|
return ConversationHandler.END
|
||||||
|
|
||||||
|
if state_key == -1:
|
||||||
|
await finalize_flow(update, context)
|
||||||
|
return ConversationHandler.END
|
||||||
|
|
||||||
|
next_step = _find_step(flow, state_key)
|
||||||
|
if not next_step:
|
||||||
|
await update.message.reply_text("Error: No se encontró el siguiente paso del flujo.")
|
||||||
|
return ConversationHandler.END
|
||||||
|
|
||||||
|
reply_markup = ReplyKeyboardRemove()
|
||||||
|
if next_step.get("type") == "keyboard" and "options" in next_step:
|
||||||
|
reply_markup = _build_keyboard(next_step["options"])
|
||||||
|
|
||||||
|
await update.message.reply_text(next_step["question"], reply_markup=reply_markup)
|
||||||
|
context.user_data["current_state"] = state_key
|
||||||
|
|
||||||
|
if next_step.get("type") == "info":
|
||||||
|
state_key = _determine_next_state(next_step, None)
|
||||||
|
if state_key is None:
|
||||||
|
await update.message.reply_text("No se pudo continuar con el flujo actual. Intenta iniciar de nuevo.")
|
||||||
|
return ConversationHandler.END
|
||||||
|
continue
|
||||||
|
|
||||||
|
return state_key
|
||||||
|
|
||||||
|
|
||||||
def create_handler(flow: dict):
|
def create_handler(flow: dict):
|
||||||
states = {}
|
states = {}
|
||||||
all_states = sorted(list(set([step["state"] for step in flow["steps"]])))
|
all_states = sorted(list(set([step["state"] for step in flow["steps"]])))
|
||||||
|
|
||||||
for state_key in all_states:
|
for state_key in all_states:
|
||||||
# Skip the end state
|
|
||||||
if state_key == -1:
|
if state_key == -1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
callback = partial(generic_callback, flow=flow)
|
callback = partial(generic_callback, flow=flow)
|
||||||
states[state_key] = [MessageHandler(filters.TEXT & ~filters.COMMAND, callback)]
|
states[state_key] = [MessageHandler(filters.TEXT & ~filters.COMMAND, callback)]
|
||||||
|
|
||||||
@@ -112,6 +160,40 @@ def create_handler(flow: dict):
|
|||||||
allow_reentry=True,
|
allow_reentry=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def end_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
await update.message.reply_text("Flujo cancelado.")
|
||||||
|
return ConversationHandler.END
|
||||||
|
|
||||||
|
|
||||||
|
async def generic_callback(update: Update, context: ContextTypes.DEFAULT_TYPE, flow: dict):
|
||||||
|
current_state_key = context.user_data.get("current_state", 0)
|
||||||
|
current_step = _find_step(flow, current_state_key)
|
||||||
|
|
||||||
|
if not current_step:
|
||||||
|
await update.message.reply_text("Hubo un error en el flujo. Por favor, inicia de nuevo.")
|
||||||
|
return ConversationHandler.END
|
||||||
|
|
||||||
|
user_answer = update.message.text
|
||||||
|
variable_name = current_step.get("variable")
|
||||||
|
if variable_name:
|
||||||
|
context.user_data[variable_name] = user_answer
|
||||||
|
|
||||||
|
next_state_key = _determine_next_state(current_step, user_answer)
|
||||||
|
if next_state_key is None:
|
||||||
|
return await end_cancel(update, context)
|
||||||
|
|
||||||
|
return await _go_to_state(update, context, flow, next_state_key)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_flow(update: Update, context: ContextTypes.DEFAULT_TYPE, flow: dict):
|
||||||
|
context.user_data.clear()
|
||||||
|
context.user_data["flow_name"] = flow["flow_name"]
|
||||||
|
|
||||||
|
first_state = flow["steps"][0]["state"]
|
||||||
|
return await _go_to_state(update, context, flow, first_state)
|
||||||
|
|
||||||
|
|
||||||
def load_flows():
|
def load_flows():
|
||||||
flow_handlers = []
|
flow_handlers = []
|
||||||
flow_dir = "conv-flows"
|
flow_dir = "conv-flows"
|
||||||
@@ -125,6 +207,7 @@ def load_flows():
|
|||||||
with open(filepath, "r", encoding="utf-8") as f:
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
try:
|
try:
|
||||||
flow_definition = json.load(f)
|
flow_definition = json.load(f)
|
||||||
|
_preprocess_flow(flow_definition)
|
||||||
handler = create_handler(flow_definition)
|
handler = create_handler(flow_definition)
|
||||||
flow_handlers.append(handler)
|
flow_handlers.append(handler)
|
||||||
logging.info(f"Flow '{flow_definition['flow_name']}' loaded successfully.")
|
logging.info(f"Flow '{flow_definition['flow_name']}' loaded successfully.")
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
|
|
||||||
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(
|
await update.message.reply_text(
|
||||||
"Proceso cancelado. ⏸️\nPuedes retomarlo con /welcome o ir al menú con /start.",
|
"Proceso cancelado. ⏸️\nPuedes retomarlo con /registro (alias /welcome) o ir al menú con /start.",
|
||||||
reply_markup=main_actions_keyboard()
|
reply_markup=main_actions_keyboard()
|
||||||
)
|
)
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
@@ -450,8 +450,11 @@ states[34] = [MessageHandler(filters.TEXT & ~filters.COMMAND, finalizar)]
|
|||||||
|
|
||||||
# Handler listo para importar en main.py
|
# Handler listo para importar en main.py
|
||||||
onboarding_handler = ConversationHandler(
|
onboarding_handler = ConversationHandler(
|
||||||
entry_points=[CommandHandler("welcome", start)], # Cambiado a /welcome
|
entry_points=[
|
||||||
states=states, # Tu diccionario de estados
|
CommandHandler("welcome", start),
|
||||||
|
CommandHandler("registro", start),
|
||||||
|
],
|
||||||
|
states=states,
|
||||||
fallbacks=[CommandHandler("cancelar", cancelar)],
|
fallbacks=[CommandHandler("cancelar", cancelar)],
|
||||||
allow_reentry=True
|
allow_reentry=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ def main_actions_keyboard(is_registered: bool = False) -> ReplyKeyboardMarkup:
|
|||||||
"""Teclado inferior con comandos directos (un toque lanza el flujo)."""
|
"""Teclado inferior con comandos directos (un toque lanza el flujo)."""
|
||||||
keyboard = []
|
keyboard = []
|
||||||
if not is_registered:
|
if not is_registered:
|
||||||
keyboard.append(["/welcome"])
|
keyboard.append(["/registro"])
|
||||||
|
|
||||||
keyboard.extend([
|
keyboard.extend([
|
||||||
["/vacaciones", "/permiso"],
|
["/vacaciones", "/permiso"],
|
||||||
|
|||||||
Reference in New Issue
Block a user