diff --git a/.env.example b/.env.example index f98de24..154fe6d 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,15 @@ WEBHOOK_PERMISOS=url WEBHOOK_PRINTS=url WEBHOOK_SCHEDULE=url +# =============================== +# LINKS +# =============================== +LINK_CURSOS=https://cursos.vanityexperience.mx/dashboard-2/ +LINK_SITIO=https://vanityexperience.mx/ +LINK_AGENDA_IOS=https://apps.apple.com/us/app/fresha-for-business/id1455346253 +LINK_AGENDA_ANDROID=https://play.google.com/store/apps/details?id=com.fresha.Business + + # =============================== # DATABASE SETUP # =============================== diff --git a/conv-flows/onboarding.json b/conv-flows/onboarding.json index ebda699..719672f 100644 --- a/conv-flows/onboarding.json +++ b/conv-flows/onboarding.json @@ -4,232 +4,318 @@ { "state": 0, "variable": "NOMBRE_SALUDO", - "question": "Para empezar con el pie derecho, ¿cómo te gusta que te llamemos?", - "type": "text" + "question": "¿Cómo te gusta que te llamemos?", + "type": "text", + "next_step": 1 }, { "state": 1, "variable": "NOMBRE_COMPLETO", - "question": "¡Lindo nombre! ✨\n\nNecesito tus datos oficiales para el contrato.\n¿Cuáles son tus *nombres* (sin apellidos) tal cual aparecen en tu INE?", - "type": "text" + "question": "Escribe tus nombres (SIN apellidos), exactamente como aparecen en tu INE.", + "type": "text", + "next_step": 2 }, { "state": 2, "variable": "APELLIDO_PATERNO", - "question": "¿Cuál es tu *apellido paterno*?", - "type": "text" + "question": "Apellido paterno:", + "type": "text", + "next_step": 3 }, { "state": 3, "variable": "APELLIDO_MATERNO", - "question": "¿Y tu *apellido materno*?", - "type": "text" + "question": "Apellido materno:", + "type": "text", + "next_step": 4 }, { "state": 4, "variable": "CUMPLE_DIA", - "question": "🎂 Hablemos de ti. ¿Qué *día* es tu cumpleaños? (Escribe el número, ej: 13)", - "type": "text" + "question": "Fecha de nacimiento · Día (solo número, ej. 13)", + "type": "text", + "next_step": 5 }, { "state": 5, "variable": "CUMPLE_MES", - "question": "¿De qué *mes*? 🎉", + "question": "Fecha de nacimiento · Mes", "type": "keyboard", - "options": [ - "Enero", "Febrero", "Marzo", - "Abril", "Mayo", "Junio", - "Julio", "Agosto", "Septiembre", - "Octubre", "Noviembre", "Diciembre" - ] + "options": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], + "next_step": 6 }, { "state": 6, "variable": "CUMPLE_ANIO", - "question": "Entendido. ¿Y de qué *año*? 🗓️", - "type": "text" + "question": "Fecha de nacimiento · Año (4 dígitos)", + "type": "text", + "next_step": 7 }, { "state": 7, "variable": "ESTADO_NACIMIENTO", - "question": "🇲🇽 ¿En qué *estado de la república* naciste?", - "type": "text" + "question": "Estado de nacimiento\n\nSelecciona el estado donde naciste.\nSi no aparece, elige *Otro*.", + "type": "keyboard", + "options": ["Coahuila", "Nuevo León", "Otro"], + "next_steps": [ + { "value": "Otro", "go_to": 7.1 }, + { "value": "default", "go_to": 8 } + ] + }, + { + "state": 7.1, + "variable": "ESTADO_NACIMIENTO_OTRO", + "question": "Escribe el nombre del estado donde naciste.", + "type": "text", + "next_step": 8 }, { "state": 8, "variable": "RFC", - "question": "Pasemos a lo administrativo 📄.\n\nPor favor escribe tu *RFC* (Sin espacios):", - "type": "text" + "question": "RFC completo (13 caracteres, sin espacios):", + "type": "text", + "next_step": 9 }, { "state": 9, "variable": "CURP", - "question": "Gracias. Ahora tu *CURP*:", - "type": "text" + "question": "CURP completo (18 caracteres):", + "type": "text", + "next_step": 10 }, { "state": 10, "variable": "CORREO", - "question": "¡Súper! 📧 ¿A qué *correo electrónico* te enviamos la info?", - "type": "text" + "question": "Correo electrónico personal:", + "type": "text", + "next_step": 11 }, { "state": 11, "variable": "CELULAR", - "question": "📱 ¿Cuál es tu número de *celular* personal? (10 dígitos)", - "type": "text" + "question": "Número de celular (10 dígitos):", + "type": "text", + "next_step": 12 }, { "state": 12, "variable": "CALLE", - "question": "🏠 Registremos tu domicilio.\n\n¿En qué *calle* vives?", - "type": "text" + "question": "Domicilio · Calle:", + "type": "text", + "next_step": 13 }, { "state": 13, "variable": "NUM_EXTERIOR", - "question": "#️⃣ ¿Cuál es el *número exterior*?", - "type": "text" + "question": "Domicilio · Número exterior:", + "type": "text", + "next_step": 14 }, { "state": 14, "variable": "NUM_INTERIOR", - "question": "🚪 ¿Tienes *número interior*? (Escribe 0 si no aplica)", - "type": "text" + "question": "Domicilio · Número interior (0 si no aplica):", + "type": "text", + "next_step": 15 }, { "state": 15, "variable": "COLONIA", - "question": "🏘️ ¿Cómo se llama la *colonia*?", - "type": "text" + "question": "Domicilio · Colonia:", + "type": "text", + "next_step": 16 }, { "state": 16, "variable": "CODIGO_POSTAL", - "question": "📮 ¿Cuál es el *Código Postal*?", - "type": "text" + "question": "Código Postal (5 dígitos):", + "type": "text", + "next_step": 17 }, { "state": 17, "variable": "CIUDAD_RESIDENCIA", - "question": "¿En qué *ciudad* resides actualmente?", + "question": "Ciudad de residencia:", "type": "keyboard", - "options": ["Saltillo", "Ramos Arizpe", "Arteaga"] + "options": ["Saltillo", "Ramos Arizpe", "Arteaga", "Otro"], + "next_steps": [ + { "value": "Otro", "go_to": 17.1 }, + { "value": "default", "go_to": 18 } + ] + }, + { + "state": 17.1, + "variable": "CIUDAD_RESIDENCIA_OTRO", + "question": "Escribe tu ciudad de residencia:", + "type": "text", + "next_step": 18 }, { "state": 18, "variable": "ROL", - "question": "🔎 *Rol dentro del equipo*\nElige la opción que mejor describa tu posición:\n• *Belleza* — servicios de estética y spa\n• *Staff (Recepción)* — agenda y atención a clientes\n• *Marketing* — contenido, promos y comunidad\n\n_Toca un botón o escribe la opción:_", + "question": "Rol dentro del equipo:", "type": "keyboard", - "options": ["Belleza", "Staff (Recepción)", "Marketing"] + "options": ["Belleza", "Staff (Recepción)", "Marketing"], + "next_step": 19 }, { "state": 19, "variable": "SUCURSAL", - "question": "¿A qué *sucursal* te vas a integrar? 📍", + "question": "Sucursal principal:", "type": "keyboard", - "options": ["Plaza Cima (Sur) ⛰️", "Plaza O (Carranza) 🏙️"] + "options": ["Plaza Cima (Sur)", "Plaza O (Carranza)"], + "next_step": 20 }, { "state": 20, "variable": "INICIO_DIA", - "question": "¡Qué emoción! 🎉\n\n¿Qué *día* está programado tu ingreso? (Solo el número, ej: 01)", - "type": "text" + "question": "Fecha de ingreso · Día:", + "type": "text", + "next_step": 21 }, { "state": 21, "variable": "INICIO_MES", - "question": "¿De qué *mes* será tu ingreso?", + "question": "Fecha de ingreso · Mes:", "type": "keyboard", - "options": [ - "Enero", "Febrero", "Marzo", - "Abril", "Mayo", "Junio", - "Julio", "Agosto", "Septiembre", - "Octubre", "Noviembre", "Diciembre" - ] + "options": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], + "next_step": 22 }, { "state": 22, "variable": "INICIO_ANIO", - "question": "¿Y de qué *año*?", + "question": "Fecha de ingreso · Año:", "type": "keyboard", - "options": ["2020", "2021", "2022", "2023", "2024", "2025", "2026"] + "options": ["2024", "2025", "2026"], + "next_step": 23 }, { "state": 23, "variable": "REF1_NOMBRE", - "question": "Ya casi acabamos. Necesito 3 referencias.\n\n👤 *Referencia 1*: Nombre completo", - "type": "text" + "question": "Referencia 1 · Nombre completo:", + "type": "text", + "next_step": 24 }, { "state": 24, "variable": "REF1_TELEFONO", - "question": "📞 Teléfono de la Referencia 1:", - "type": "text" + "question": "Referencia 1 · Teléfono:", + "type": "text", + "next_step": 25 }, { "state": 25, "variable": "REF1_TIPO", - "question": "🧑‍🤝‍🧑 ¿Qué relación tienes con ella/él?", + "question": "Referencia 1 · Relación:", "type": "keyboard", - "options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"] + "options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"], + "next_steps": [ + { "value": "Otra", "go_to": 25.1 }, + { "value": "default", "go_to": 26 } + ] + }, + { + "state": 25.1, + "variable": "REF1_TIPO_OTRA", + "question": "Especifíca la relación con la Referencia 1:", + "type": "text", + "next_step": 26 }, { "state": 26, "variable": "REF2_NOMBRE", - "question": "Ok. Vamos con la *Referencia 2*.\n\n👤 Nombre completo:", - "type": "text" + "question": "Referencia 2 · Nombre completo:", + "type": "text", + "next_step": 27 }, { "state": 27, "variable": "REF2_TELEFONO", - "question": "📞 Teléfono de la Referencia 2:", - "type": "text" + "question": "Referencia 2 · Teléfono:", + "type": "text", + "next_step": 28 }, { "state": 28, "variable": "REF2_TIPO", - "question": "🧑‍🤝‍🧑 ¿Qué relación tienen?", + "question": "Referencia 2 · Relación:", "type": "keyboard", - "options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"] + "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, "variable": "REF3_NOMBRE", - "question": "Última. *Referencia 3*.\n\n👤 Nombre completo:", - "type": "text" + "question": "Referencia 3 · Nombre completo:", + "type": "text", + "next_step": 30 }, { "state": 30, "variable": "REF3_TELEFONO", - "question": "📞 Teléfono de la Referencia 3:", - "type": "text" + "question": "Referencia 3 · Teléfono:", + "type": "text", + "next_step": 31 }, { "state": 31, "variable": "REF3_TIPO", - "question": "🧑‍🤝‍🧑 ¿Qué relación tienen?", + "question": "Referencia 3 · Relación:", "type": "keyboard", - "options": ["Familiar", "Amistad", "Trabajo", "Académica", "Otra"] + "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, "variable": "EMERGENCIA_NOMBRE", - "question": "Finalmente, por seguridad 🚑:\n\n¿A quién llamamos en caso de *emergencia*?", - "type": "text" + "question": "Contacto de emergencia · Nombre completo:", + "type": "text", + "next_step": 33 }, { "state": 33, "variable": "EMERGENCIA_TEL", - "question": "☎️ ¿Cuál es el teléfono de esa persona?", - "type": "text" + "question": "Contacto de emergencia · Teléfono:", + "type": "text", + "next_step": 34 }, { "state": 34, "variable": "EMERGENCIA_RELACION", - "question": "¿Qué parentesco tiene contigo?", + "question": "Relación con el contacto de emergencia:", "type": "keyboard", - "options": ["Padre/Madre", "Esposo/a","Pareja", "Hijo/a", "Hermano/a", "Amigo/a", "Otro"] + "options": ["Padre/Madre", "Esposo/a", "Pareja", "Hijo/a", "Hermano/a", "Amigo/a", "Otro"], + "next_steps": [ + { "value": "Otro", "go_to": 34.1 }, + { "value": "default", "go_to": -1 } + ] + }, + { + "state": 34.1, + "variable": "EMERGENCIA_RELACION_OTRA", + "question": "Especifíca la relación con el contacto de emergencia:", + "type": "text", + "next_step": -1 } ] -} +} \ No newline at end of file diff --git a/main.py b/main.py index 25265a2..54dc1bd 100644 --- a/main.py +++ b/main.py @@ -16,17 +16,18 @@ from telegram.constants import ParseMode from telegram.ext import Application, Defaults, CommandHandler, ContextTypes # --- IMPORTAR HABILIDADES --- -from modules.onboarding import onboarding_handler -from modules.rh_requests import vacaciones_handler, permiso_handler +from modules.flow_builder import load_flows from modules.logger import log_request from modules.database import chat_id_exists # Importar chat_id_exists from modules.ui import main_actions_keyboard # from modules.finder import finder_handler (Si lo creas después) -LINK_CURSOS = "https://cursos.vanityexperience.mx/dashboard-2/" -LINK_SITIO = "https://vanityexperience.mx/" -LINK_AGENDA_IOS = "https://apps.apple.com/us/app/fresha-for-business/id1455346253" -LINK_AGENDA_ANDROID = "https://play.google.com/store/apps/details?id=com.fresha.Business" +# Cargar links desde variables de entorno +LINK_CURSOS = os.getenv("LINK_CURSOS", "https://cursos.vanityexperience.mx/dashboard-2/") +LINK_SITIO = os.getenv("LINK_SITIO", "https://vanityexperience.mx/") +LINK_AGENDA_IOS = os.getenv("LINK_AGENDA_IOS", "https://apps.apple.com/us/app/fresha-for-business/id1455346253") +LINK_AGENDA_ANDROID = os.getenv("LINK_AGENDA_ANDROID", "https://play.google.com/store/apps/details?id=com.fresha.Business") + TOKEN = os.getenv("TELEGRAM_TOKEN") @@ -112,9 +113,10 @@ def main(): app.add_handler(CommandHandler("help", menu_principal)) # 2. Habilidades Complejas (Conversaciones) - app.add_handler(onboarding_handler) - app.add_handler(vacaciones_handler) - app.add_handler(permiso_handler) + flow_handlers = load_flows() + for handler in flow_handlers: + app.add_handler(handler) + app.add_handler(CommandHandler("links", links_menu)) # app.add_handler(finder_handler) diff --git a/modules/flow_builder.py b/modules/flow_builder.py new file mode 100644 index 0000000..3fefb2f --- /dev/null +++ b/modules/flow_builder.py @@ -0,0 +1,134 @@ +import json +import os +import logging +from functools import partial +from telegram import Update, ReplyKeyboardRemove, ReplyKeyboardMarkup +from telegram.ext import ( + ContextTypes, + ConversationHandler, + MessageHandler, + CommandHandler, + filters, +) + +# Assuming finalization logic will be handled elsewhere for now +# from .onboarding import finalizar, cancelar + +# A simple end state for now +async def end(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + await update.message.reply_text("Flow ended.") + 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) + + # Find the current step in the flow + current_step = next((step for step in flow["steps"] if step["state"] == current_state_key), None) + + if not current_step: + await update.message.reply_text("Hubo un error en el flujo. Por favor, inicia de nuevo.") + return ConversationHandler.END + + # Save the answer + user_answer = update.message.text + variable_name = current_step.get("variable") + if variable_name: + context.user_data[variable_name] = user_answer + + # Determine the next state + next_state_key = None + if "next_steps" in current_step: + for condition in current_step["next_steps"]: + if condition["value"] == user_answer: + next_state_key = condition["go_to"] + break + elif condition["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: + # If no next step is defined, end the conversation + return await end(update, context) + + # Find the next step + next_step = next((step for step in flow["steps"] if step["state"] == next_state_key), None) + + if not next_step: + # If the next step is the end of the conversation + if next_state_key == -1: + # Here we would call the generic "finalizar" function + # For now, just end it + await update.message.reply_text("Has completado el flujo. ¡Gracias!") + # return await finalizar(update, context) + return ConversationHandler.END + else: + await update.message.reply_text("Error: No se encontró el siguiente paso del flujo.") + return ConversationHandler.END + + # Ask the next question + reply_markup = ReplyKeyboardRemove() + if next_step.get("type") == "keyboard" and "options" in next_step: + reply_markup = ReplyKeyboardMarkup( + [next_step["options"][i:i+3] for i in range(0, len(next_step["options"]), 3)], + one_time_keyboard=True, resize_keyboard=True + ) + + await update.message.reply_text(next_step["question"], reply_markup=reply_markup) + + # Update the current state + context.user_data["current_state"] = next_state_key + return 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"] + + # Start with the first step + first_step = flow["steps"][0] + context.user_data["current_state"] = first_step["state"] + + reply_markup = ReplyKeyboardRemove() + if first_step.get("type") == "keyboard" and "options" in first_step: + reply_markup = ReplyKeyboardMarkup( + [first_step["options"][i:i+3] for i in range(0, len(first_step["options"]), 3)], + one_time_keyboard=True, resize_keyboard=True + ) + + await update.message.reply_text(first_step["question"], reply_markup=reply_markup) + return first_step["state"] + + +def create_handler(flow: dict): + states = {} + for step in flow["steps"]: + callback = partial(generic_callback, flow=flow) + states[step["state"]] = [MessageHandler(filters.TEXT & ~filters.COMMAND, callback)] + + # The entry point should be a command with the same name as the flow + entry_point = CommandHandler(flow["flow_name"], partial(start_flow, flow=flow)) + + return ConversationHandler( + entry_points=[entry_point], + states=states, + fallbacks=[CommandHandler("cancelar", end)], # Replace with generic cancel + allow_reentry=True, + ) + +def load_flows(): + flow_handlers = [] + for filename in os.listdir("conv-flows"): + if filename.endswith(".json"): + filepath = os.path.join("conv-flows", filename) + with open(filepath, "r", encoding="utf-8") as f: + try: + flow_definition = json.load(f) + handler = create_handler(flow_definition) + flow_handlers.append(handler) + logging.info(f"Flow '{flow_definition['flow_name']}' loaded successfully.") + except json.JSONDecodeError as e: + logging.error(f"Error decoding JSON from {filename}: {e}") + except Exception as e: + logging.error(f"Error creating handler for {filename}: {e}") + return flow_handlers