From 8422a874d94025be8cd0e65e0f9d7e17f6232906 Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Mon, 15 Dec 2025 09:49:50 -0600 Subject: [PATCH] feat: Implement a new links menu, consolidate the main action keyboard into a `ui` module, and update employee ID generation logic. --- Readme.md | 4 +-- docker-compose.collify.yml | 2 +- main.py | 67 +++++++++++++++++++++++++++++++------- modules/ai.py | 14 ++++++++ modules/onboarding.py | 23 +++++++++---- modules/rh_requests.py | 21 +++++++++--- modules/ui.py | 13 ++++++++ 7 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 modules/ui.py diff --git a/Readme.md b/Readme.md index bd5ecc2..b8d57be 100644 --- a/Readme.md +++ b/Readme.md @@ -90,13 +90,13 @@ Si Collify solo consume imágenes ya publicadas, usa el archivo `docker-compose. 1) Construir y publicar la imagen (ejemplo con Buildx y tag con timestamp): ```bash -export DOCKER_IMAGE=registry.example.com/vanessa-bot:prod-$(date +%Y%m%d%H%M) +export DOCKER_IMAGE=marcogll/vanessa-bot:prod-$(date +%Y%m%d%H%M) docker buildx build --platform linux/amd64 -t $DOCKER_IMAGE . --push ``` 2) Desplegar en el servidor (Collify) usando la imagen publicada: ```bash -export DOCKER_IMAGE=registry.example.com/vanessa-bot:prod-20240101 +export DOCKER_IMAGE=marcogll/vanessa-bot:prod-20240101 docker compose -f docker-compose.collify.yml pull docker compose -f docker-compose.collify.yml up -d ``` diff --git a/docker-compose.collify.yml b/docker-compose.collify.yml index 807c09a..1c3a105 100644 --- a/docker-compose.collify.yml +++ b/docker-compose.collify.yml @@ -2,7 +2,7 @@ version: "3.8" services: bot: - image: ${DOCKER_IMAGE:-vanessa-bot:latest} + image: marcogll/vanessa-bot:latest container_name: vanessa_bot restart: always env_file: diff --git a/main.py b/main.py index ed96f84..d7eada9 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,17 @@ import os import logging from dotenv import load_dotenv +from typing import Optional # Cargar variables de entorno antes de importar módulos que las usan load_dotenv() -from telegram import Update, ReplyKeyboardMarkup, BotCommand +from telegram import ( + Update, + InlineKeyboardButton, + InlineKeyboardMarkup, + BotCommand, +) from telegram.constants import ParseMode from telegram.ext import Application, Defaults, CommandHandler, ContextTypes @@ -13,30 +19,67 @@ from telegram.ext import Application, Defaults, CommandHandler, ContextTypes from modules.onboarding import onboarding_handler from modules.rh_requests import vacaciones_handler, permiso_handler from modules.database import log_request +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" TOKEN = os.getenv("TELEGRAM_TOKEN") logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) +def _guess_platform(update: Update) -> Optional[str]: + """ + Telegram no expone el OS del usuario en mensajes regulares. + Devolvemos None para mostrar ambos links; si en el futuro llegan datos, se pueden mapear aquí. + """ + try: + _ = update.to_dict() # placeholder por si queremos inspeccionar el payload + except Exception: + pass + return None + +async def links_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Muestra accesos rápidos a cursos, sitio y descargas.""" + user = update.effective_user + log_request(user.id, user.username, "links", update.message.text) + + plataforma = _guess_platform(update) + descarga_buttons = [] + if plataforma == "ios": + descarga_buttons.append(InlineKeyboardButton("Agenda | iOS", url=LINK_AGENDA_IOS)) + elif plataforma == "android": + descarga_buttons.append(InlineKeyboardButton("Agenda | Android", url=LINK_AGENDA_ANDROID)) + else: + descarga_buttons = [ + InlineKeyboardButton("Agenda | iOS", url=LINK_AGENDA_IOS), + InlineKeyboardButton("Agenda | Android", url=LINK_AGENDA_ANDROID), + ] + + texto = ( + "🌐 Links útiles\n" + "Claro, aquí tienes enlaces que puedes necesitar durante tu estancia con nosotros:\n" + "Toca el que te aplique." + ) + botones = [ + [InlineKeyboardButton("Cursos Vanity", url=LINK_CURSOS)], + [InlineKeyboardButton("Sitio Vanity", url=LINK_SITIO)], + descarga_buttons, + ] + await update.message.reply_text(texto, reply_markup=InlineKeyboardMarkup(botones)) + async def menu_principal(update: Update, context: ContextTypes.DEFAULT_TYPE): """Muestra el menú de opciones de Vanessa""" user = update.effective_user log_request(user.id, user.username, "start", update.message.text) texto = ( "👩‍💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\n" - "Comandos rápidos:\n" - "/welcome — Registro de nuevas empleadas\n" - "/vacaciones — Solicitud de vacaciones\n" - "/permiso — Solicitud de permiso por horas\n\n" - "También tienes los botones rápidos abajo 👇" + "Toca un botón para continuar 👇" ) - teclado = ReplyKeyboardMarkup( - [["/welcome"], ["/vacaciones", "/permiso"]], - resize_keyboard=True - ) - await update.message.reply_text(texto, reply_markup=teclado) + await update.message.reply_text(texto, reply_markup=main_actions_keyboard()) async def post_init(application: Application): # Mantén los comandos rápidos disponibles en el menú de Telegram @@ -45,6 +88,7 @@ async def post_init(application: Application): BotCommand("welcome", "Registro de nuevas empleadas"), BotCommand("vacaciones", "Solicitar vacaciones"), BotCommand("permiso", "Solicitar permiso por horas"), + BotCommand("links", "Links útiles"), BotCommand("cancelar", "Cancelar flujo actual"), ]) @@ -69,6 +113,7 @@ def main(): 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(finder_handler) print("🧠 Vanessa Bot Brain iniciada y lista para trabajar en todos los módulos.") diff --git a/modules/ai.py b/modules/ai.py index c0cf541..a68718f 100644 --- a/modules/ai.py +++ b/modules/ai.py @@ -1,4 +1,18 @@ import os +import importlib +import importlib.metadata as importlib_metadata + +# Compatibilidad para entornos donde packages_distributions no existe (p.ej. Python 3.9 con importlib recortado). +if not hasattr(importlib_metadata, "packages_distributions"): + try: + import importlib_metadata as backport_metadata # type: ignore + if hasattr(backport_metadata, "packages_distributions"): + importlib_metadata.packages_distributions = backport_metadata.packages_distributions # type: ignore[attr-defined] + else: + importlib_metadata.packages_distributions = lambda: {} # type: ignore[assignment] + except Exception: + importlib_metadata.packages_distributions = lambda: {} # type: ignore[assignment] + import google.generativeai as genai def classify_reason(text: str) -> str: diff --git a/modules/onboarding.py b/modules/onboarding.py index 4c5aa0e..e4bbe36 100644 --- a/modules/onboarding.py +++ b/modules/onboarding.py @@ -18,6 +18,7 @@ from telegram.ext import ( ) from modules.database import log_request +from modules.ui import main_actions_keyboard # --- 1. CARGA DE ENTORNO --- load_dotenv() # Carga las variables del archivo .env @@ -304,8 +305,14 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: fecha_ini = "ERROR_FECHA" # Derivados num_ext_texto = numero_a_texto(r.get(NUM_EXTERIOR, ""), r.get(NUM_INTERIOR, "")) - curp_base = (r.get(CURP) or "").upper() - n_empleado = f"{(curp_base + 'XXXX')[:4]}{fecha_ini.replace('-', '')}" + # El número de empleado debe ser solo la fecha de inicio en formato AAMMDD. + try: + fecha_inicio_dt = datetime.strptime(fecha_ini, "%Y-%m-%d") + n_empleado = fecha_inicio_dt.strftime("%y%m%d") + except Exception: + # Fallback defensivo para no romper el flujo si viene un formato raro. + fecha_compacta = fecha_ini.replace("-", "") + n_empleado = fecha_compacta[-6:] if len(fecha_compacta) >= 6 else fecha_compacta or "N/A" # PAYLOAD ESTRUCTURADO PARA N8N payload = { @@ -378,18 +385,22 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: await update.message.reply_text( "✅ *¡Registro Exitoso!*\n\n" "Bienvenida a la familia Soul/Vanity. Tu contrato se está generando y te avisaremos pronto.\n" - "¡Nos vemos el primer día! ✨" + "¡Nos vemos el primer día! ✨", + reply_markup=main_actions_keyboard() ) else: - await update.message.reply_text("⚠️ Se guardaron tus datos pero hubo un error de conexión. RH lo revisará manualmente.") + await update.message.reply_text( + "⚠️ Se guardaron tus datos pero hubo un error de conexión. RH lo revisará manualmente.", + reply_markup=main_actions_keyboard() + ) context.user_data.clear() return ConversationHandler.END async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: await update.message.reply_text( - "Proceso cancelado. ⏸️\nCuando quieras retomar, escribe /contrato.", - reply_markup=ReplyKeyboardRemove() + "Proceso cancelado. ⏸️\nPuedes retomarlo con /welcome o ir al menú con /start.", + reply_markup=main_actions_keyboard() ) context.user_data.clear() return ConversationHandler.END diff --git a/modules/rh_requests.py b/modules/rh_requests.py index 84d2089..9b6b99b 100644 --- a/modules/rh_requests.py +++ b/modules/rh_requests.py @@ -5,6 +5,7 @@ from datetime import datetime, date from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters from modules.database import log_request +from modules.ui import main_actions_keyboard from modules.ai import classify_reason # Helpers de webhooks @@ -342,18 +343,30 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) f"- Estatus inicial: {payload.get('status_inicial', 'N/A')}" ) if enviados > 0: - await update.message.reply_text(f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.\n\n{resumen}") + await update.message.reply_text( + f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.\n\n{resumen}", + reply_markup=main_actions_keyboard() + ) else: - await update.message.reply_text(f"⚠️ No hay webhook configurado o falló el envío. RH lo revisará.\n\n{resumen}") + await update.message.reply_text( + f"⚠️ No hay webhook configurado o falló el envío. RH lo revisará.\n\n{resumen}", + reply_markup=main_actions_keyboard() + ) 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.", + reply_markup=main_actions_keyboard() + ) return ConversationHandler.END async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - await update.message.reply_text("Solicitud cancelada.") + await update.message.reply_text( + "Solicitud cancelada. ⏸️\nPuedes volver a iniciar con /vacaciones o /permiso, o ir al menú con /start.", + reply_markup=main_actions_keyboard(), + ) return ConversationHandler.END # Handlers separados pero comparten lógica diff --git a/modules/ui.py b/modules/ui.py new file mode 100644 index 0000000..7bb0c5d --- /dev/null +++ b/modules/ui.py @@ -0,0 +1,13 @@ +from telegram import ReplyKeyboardMarkup + + +def main_actions_keyboard() -> ReplyKeyboardMarkup: + """Teclado inferior con comandos directos (un toque lanza el flujo).""" + return ReplyKeyboardMarkup( + [ + ["/welcome"], + ["/vacaciones", "/permiso"], + ["/links", "/start"], + ], + resize_keyboard=True, + )