docs: Translate comments and logging messages to Spanish across various modules and the scheduler.

This commit is contained in:
Marco Gallegos
2025-12-18 00:17:14 -06:00
parent ade8a5f98d
commit e960538943
16 changed files with 266 additions and 230 deletions

View File

@@ -1,18 +1,15 @@
# app/modules/admin.py
"""
This module contains administrative functions for the bot.
Currently, it provides a simple way to check the system's status.
"""
# Este módulo contiene funciones administrativas para el bot.
# Por ahora, permite ver el estado general del sistema.
def get_system_status():
"""
Returns a formatted string with the current status of the bot and its integrations.
This function currently returns a hardcoded status message. In the future,
it could be expanded to perform real-time checks on the different services.
Devuelve un mensaje con el estado actual del bot y sus conexiones.
Actualmente el mensaje es fijo (hardcoded), pero en el futuro podría
hacer pruebas reales de conexión.
"""
# TODO: Implement real-time status checks for more accurate monitoring.
# TODO: Implementar pruebas de estado en tiempo real para un monitoreo exacto.
status_text = (
"📊 *Estado del Sistema*\n\n"
"- *Bot Principal:* Activo ✅\n"

View File

@@ -1,19 +1,15 @@
# app/modules/agenda.py
"""
This module is responsible for handling agenda-related requests.
It provides functionality to fetch and display the user's schedule for the day.
"""
# Este módulo se encarga de manejar las peticiones relacionadas con la agenda.
# Permite obtener y mostrar las actividades programadas para el día.
def get_agenda():
"""
Fetches and displays the user's agenda for the current day.
Currently, this function returns a hardcoded sample agenda for demonstration
purposes. The plan is to replace this with a real integration that fetches
events from a service like Google Calendar.
Obtiene y muestra la agenda del usuario para el día actual.
Por ahora, esta función devuelve una agenda de ejemplo fija.
El plan es conectarla con Google Calendar para que sea real.
"""
# TODO: Fetch the agenda dynamically from Google Calendar.
# TODO: Obtener la agenda dinámicamente desde Google Calendar.
agenda_text = (
"📅 *Agenda para Hoy*\n\n"
"• *10:00 AM - 11:00 AM*\n"

View File

@@ -1,22 +1,19 @@
# app/modules/aprobaciones.py
"""
This module manages the approval workflow for requests made by the team.
# Este módulo gestiona el flujo de aprobación para las solicitudes hechas por el equipo.
# Permite ver solicitudes pendientes y aprobarlas o rechazarlas.
# El usuario principal aquí es el "owner" (dueño).
It provides functions to view pending requests and to handle the approval or
rejection of those requests. The primary user for this module is the "owner"
role, who has the authority to approve or deny requests.
"""
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
def get_approval_menu(request_id):
"""
Creates and returns an inline keyboard with "Approve" and "Reject" buttons.
Each button is associated with a specific request_id through the
callback_data, allowing the bot to identify which request is being acted upon.
Crea un menú de botones (teclado en línea) con "Aprobar" y "Rechazar".
Cada botón lleva el ID de la solicitud para saber cuál estamos procesando.
"""
keyboard = [
[
# callback_data es lo que el bot recibe cuando se pulsa el botón
InlineKeyboardButton("✅ Aprobar", callback_data=f'approve:{request_id}'),
InlineKeyboardButton("❌ Rechazar", callback_data=f'reject:{request_id}'),
]
@@ -25,13 +22,11 @@ def get_approval_menu(request_id):
def view_pending():
"""
Shows the owner a list of pending requests that require their attention.
Currently, this function uses a hardcoded list of proposals for demonstration.
In a production environment, this would fetch data from a database or another
persistent storage mechanism where pending requests are tracked.
Muestra al dueño una lista de solicitudes que esperan su aprobación.
Por ahora usa una lista fija de ejemplo.
"""
# TODO: Fetch pending requests dynamically from a database or webhook events.
# TODO: Obtener solicitudes reales desde una base de datos o servicio externo.
proposals = [
{"id": "prop_001", "desc": "Grabación de proyecto", "duration": 4, "user": "Equipo A"},
{"id": "prop_002", "desc": "Taller de guion", "duration": 2, "user": "Equipo B"},
@@ -40,7 +35,7 @@ def view_pending():
if not proposals:
return "No hay solicitudes pendientes.", None
# For demonstration purposes, we'll just show the first pending proposal.
# Tomamos la primera propuesta para mostrarla
proposal = proposals[0]
text = (
@@ -50,28 +45,25 @@ def view_pending():
f"⏳ *Duración:* {proposal['duration']} horas"
)
# Attach the approval menu to the message.
# Adjuntamos los botones de aprobación
reply_markup = get_approval_menu(proposal['id'])
return text, reply_markup
def handle_approval_action(callback_data):
"""
Handles the owner's response (approve or reject) to a request.
This function is triggered when the owner clicks one of the buttons created
by get_approval_menu. It parses the callback_data to determine the action
and the request ID.
Maneja la respuesta del dueño (clic en aprobar o rechazar).
Separa la acción (approve/reject) del ID de la solicitud.
"""
# callback_data viene como "accion:id", por ejemplo "approve:prop_001"
action, request_id = callback_data.split(':')
if action == 'approve':
# TODO: Implement logic to update the request's status to 'approved'.
# This could involve updating a database and notifying the requester.
# TODO: Guardar en base de datos que fue aprobada y avisar al equipo.
return f"✅ La solicitud *{request_id}* ha sido aprobada."
elif action == 'reject':
# TODO: Implement logic to update the request's status to 'rejected'.
# This could involve updating a database and notifying the requester.
# TODO: Guardar en base de datos que fue rechazada y avisar al equipo.
return f"❌ La solicitud *{request_id}* ha sido rechazada."
return "Acción desconocida.", None

View File

@@ -1,21 +1,15 @@
# app/modules/citas.py
"""
This module handles appointment scheduling for clients.
It provides a simple way for users to get a link to an external scheduling
service, such as Calendly or an n8n workflow.
"""
# Este módulo maneja la programación de citas para los clientes.
# Permite a los usuarios obtener un enlace para agendar una reunión.
def request_appointment():
"""
Provides the user with a link to schedule an appointment.
Currently, this function returns a hardcoded placeholder link to Calendly.
The intention is to replace this with a dynamic link generated by an n8n
workflow or another scheduling service.
Proporciona al usuario un enlace para agendar una cita.
Por ahora devuelve un enlace de ejemplo a Calendly.
La idea es que sea un enlace dinámico generado por n8n.
"""
# TODO: Integrate with a real scheduling service or an n8n workflow to
# provide a dynamic and personalized scheduling link.
# TODO: Integrar con un servicio real o un flujo de n8n para dar un enlace personalizado.
response_text = (
"Para agendar una cita, por favor utiliza el siguiente enlace: \n\n"
"[Enlace de Calendly](https://calendly.com/user/appointment-link)"

View File

@@ -1,12 +1,8 @@
# app/modules/create_tag.py
"""
This module contains the functionality for the /create_tag command.
# Este módulo permite crear un "tag" (etiqueta) con información del empleado.
# Usa un ConversationHandler para hacer una serie de preguntas al usuario.
# Al final, genera un código en Base64 que contiene toda la información en formato JSON.
It uses a ConversationHandler to guide the user through a series of questions
to collect data (name, employee number, branch, and Telegram ID), and then
generates a Base64-encoded JSON string from that data. This string is intended
to be used for creating an NFC tag.
"""
import base64
import json
import logging
@@ -19,62 +15,56 @@ from telegram.ext import (
filters,
)
# Enable logging to monitor the bot's operation and for debugging.
# Configuramos los logs para este archivo
logger = logging.getLogger(__name__)
# Define the states for the conversation. These states are used to track the
# user's progress through the conversation and determine which handler function
# should be executed next.
# Definimos los estados de la conversación.
# Cada número representa un paso en el proceso de preguntas.
NAME, NUM_EMP, SUCURSAL, TELEGRAM_ID = range(4)
async def create_tag_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Starts the conversation to create a new tag when the /create_tag command
is issued. It prompts the user for the first piece of information (name).
Inicia el proceso cuando el usuario escribe /create_tag.
Pide el primer dato: el nombre.
"""
await update.message.reply_text("Vamos a crear un nuevo tag. Por favor, dime el nombre:")
# The function returns the next state, which is NAME, so the conversation
# knows which handler to call next.
# Devolvemos el siguiente estado: NAME
return NAME
async def get_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Stores the user's provided name in the context and then asks for the
next piece of information, the employee number.
Guarda el nombre y pide el número de empleado.
"""
context.user_data['name'] = update.message.text
await update.message.reply_text("Gracias. Ahora, por favor, dime el número de empleado:")
# The function returns the next state, NUM_EMP.
# Devolvemos el siguiente estado: NUM_EMP
return NUM_EMP
async def get_num_emp(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Stores the employee number and proceeds to ask for the branch name.
Guarda el número de empleado y pide la sucursal.
"""
context.user_data['num_emp'] = update.message.text
await update.message.reply_text("Entendido. Ahora, por favor, dime la sucursal:")
# The function returns the next state, SUCURSAL.
# Devolvemos el siguiente estado: SUCURSAL
return SUCURSAL
async def get_sucursal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Stores the branch name and asks for the final piece of information,
the user's Telegram ID.
Guarda la sucursal y pide el ID de Telegram.
"""
context.user_data['sucursal'] = update.message.text
await update.message.reply_text("Perfecto. Finalmente, por favor, dime el ID de Telegram:")
# The function returns the next state, TELEGRAM_ID.
# Devolvemos el siguiente estado: TELEGRAM_ID
return TELEGRAM_ID
async def get_telegram_id(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Stores the Telegram ID, assembles all the collected data into a JSON
object, encodes it into a Base64 string, and sends the result back to
the user. This function concludes the conversation.
Guarda el ID de Telegram, junta todos los datos y genera el código Base64.
"""
context.user_data['telegram_id'] = update.message.text
# Create a dictionary from the data collected and stored in user_data.
# Creamos un diccionario (como una caja con etiquetas) con todos los datos
tag_data = {
"name": context.user_data.get('name'),
"num_emp": context.user_data.get('num_emp'),
@@ -82,28 +72,27 @@ async def get_telegram_id(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
"telegram_id": context.user_data.get('telegram_id'),
}
# Convert the Python dictionary into a JSON formatted string.
# Convertimos el diccionario a una cadena de texto en formato JSON
json_string = json.dumps(tag_data)
# Encode the JSON string into Base64. The string is first encoded to
# UTF-8 bytes, which is then encoded to Base64 bytes, and finally
# decoded back to a UTF-8 string for display.
# Convertimos esa cadena a Base64 (un formato que se puede guardar en tags NFC)
# 1. Codificamos a bytes (utf-8)
# 2. Codificamos esos bytes a base64
# 3. Convertimos de vuelta a texto para mostrarlo
base64_bytes = base64.b64encode(json_string.encode('utf-8'))
base64_string = base64_bytes.decode('utf-8')
await update.message.reply_text(f"¡Gracias! Aquí está tu tag en formato Base64:\n\n`{base64_string}`", parse_mode='Markdown')
# Clean up the user_data dictionary to ensure no data from this
# conversation is accidentally used in another one.
# Limpiamos los datos temporales del usuario
context.user_data.clear()
# End the conversation.
# Terminamos la conversación
return ConversationHandler.END
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Cancels and ends the conversation if the user issues the /cancel command.
It also clears any data that has been collected so far.
Cancela el proceso si el usuario escribe /cancel.
"""
await update.message.reply_text("Creación de tag cancelada.")
context.user_data.clear()
@@ -111,19 +100,13 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
def create_tag_conv_handler():
"""
Creates and returns a ConversationHandler for the /create_tag command.
This handler manages the entire conversational flow, from starting the
conversation to handling user inputs and ending the conversation.
Configura el manejador de la conversación (el flujo de preguntas).
"""
return ConversationHandler(
# The entry_points list defines how the conversation can be started.
# In this case, it's started by the /create_tag command.
# Punto de entrada: el comando /create_tag
entry_points=[CommandHandler('create_tag', create_tag_start)],
# The states dictionary maps the conversation states to their
# respective handler functions. When the conversation is in a
# particular state, the corresponding handler is called to process
# the user's message.
# Mapa de estados: qué función responde a cada paso
states={
NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
NUM_EMP: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_num_emp)],
@@ -131,12 +114,8 @@ def create_tag_conv_handler():
TELEGRAM_ID: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_telegram_id)],
},
# The fallbacks list defines handlers that are called if the user
# sends a message that doesn't match the current state's handler.
# Here, it's used to handle the /cancel command.
# Si algo falla o el usuario cancela
fallbacks=[CommandHandler('cancel', cancel)],
# per_message=False means the conversation is tied to the user, not
# to a specific message, which is standard for this type of flow.
per_message=False
)

View File

@@ -1,45 +1,42 @@
# app/modules/equipo.py
"""
This module contains functionality for authorized team members.
# Este módulo contiene funciones para los miembros autorizados del equipo.
# Incluye un flujo para proponer actividades que el dueño debe aprobar.
It includes a conversational flow for proposing new activities that require
approval from the owner, as well as a function to check the status of
previously submitted requests.
"""
from telegram import Update
from telegram.ext import ContextTypes, ConversationHandler
# Define the states for the activity proposal conversation.
# Definimos los estados para la conversación de propuesta de actividad.
DESCRIPTION, DURATION = range(2)
async def propose_activity_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Starts the conversation for a team member to propose a new activity.
This is typically triggered by an inline button press.
Inicia el proceso para que un miembro del equipo proponga una actividad.
Se activa cuando se pulsa el botón correspondiente.
"""
await update.callback_query.answer()
await update.callback_query.edit_message_text(
"Por favor, describe la actividad que quieres proponer."
)
# The function returns the next state, which is DESCRIPTION.
# Siguiente paso: DESCRIPTION
return DESCRIPTION
async def get_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Stores the activity description provided by the user and asks for the duration.
Guarda la descripción de la actividad y pide la duración.
"""
context.user_data['activity_description'] = update.message.text
await update.message.reply_text(
"Entendido. Ahora, por favor, indica la duración estimada en horas (ej. 2, 4.5)."
)
# The function returns the next state, DURATION.
# Siguiente paso: DURATION
return DURATION
async def get_duration(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Stores the activity duration, confirms the proposal to the user, and ends the conversation.
Guarda la duración, confirma la propuesta y termina la conversación.
"""
try:
# Intentamos convertir el texto a un número decimal (float)
duration = float(update.message.text)
context.user_data['activity_duration'] = duration
description = context.user_data.get('activity_description', 'N/A')
@@ -51,24 +48,22 @@ async def get_duration(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in
"Recibirás una notificación cuando sea revisada."
)
# TODO: Send this proposal to the owner for approval, for example,
# by sending a webhook or saving it to a database.
# TODO: Enviar esta propuesta al dueño (por webhook o base de datos).
await update.message.reply_text(confirmation_text, parse_mode='Markdown')
# Clean up user_data to prevent data leakage into other conversations.
# Limpiamos los datos temporales
context.user_data.clear()
# End the conversation.
# Terminamos la conversación
return ConversationHandler.END
except ValueError:
# If the user provides an invalid number for the duration, ask again.
# Si el usuario no escribe un número válido, se lo pedimos de nuevo
await update.message.reply_text("Por favor, introduce un número válido para la duración en horas.")
return DURATION
async def cancel_proposal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""
Cancels and ends the activity proposal conversation.
This is triggered by the /cancel command.
Cancela el proceso de propuesta si el usuario escribe /cancel.
"""
await update.message.reply_text("La propuesta de actividad ha sido cancelada.")
context.user_data.clear()
@@ -76,10 +71,9 @@ async def cancel_proposal(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
def view_requests_status():
"""
Allows a team member to see the status of their recent requests.
Currently, this returns a hardcoded sample status. In a real-world
application, this would fetch the user's requests from a database.
Permite a un miembro del equipo ver el estado de sus solicitudes recientes.
Por ahora devuelve un estado de ejemplo fijo.
"""
# TODO: Fetch the status of recent requests from a persistent data source.
# TODO: Obtener el estado real desde una base de datos.
return "Aquí está el estado de tus solicitudes recientes:\n\n- Grabación de proyecto (4h): Aprobado\n- Taller de guion (2h): Pendiente"

View File

@@ -1,16 +1,11 @@
# app/modules/onboarding.py
"""
This module handles the initial interaction with the user, specifically the
/start command.
# Este módulo maneja la primera interacción con el usuario (el comando /start).
# Se encarga de mostrar un menú diferente según quién sea el usuario (dueño, admin, equipo o cliente).
It is responsible for identifying the user's role and presenting them with a
customized menu of options based on their permissions. This ensures that each
user sees only the actions relevant to them.
"""
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
def get_owner_menu():
"""Creates and returns the main menu keyboard for the 'owner' role."""
"""Crea el menú de botones para el Dueño (Owner)."""
keyboard = [
[InlineKeyboardButton("📅 Ver mi agenda", callback_data='view_agenda')],
[InlineKeyboardButton("⏳ Ver pendientes", callback_data='view_pending')],
@@ -18,7 +13,7 @@ def get_owner_menu():
return InlineKeyboardMarkup(keyboard)
def get_admin_menu():
"""Creates and returns the main menu keyboard for the 'admin' role."""
"""Crea el menú de botones para los Administradores."""
keyboard = [
[InlineKeyboardButton("📊 Ver estado del sistema", callback_data='view_system_status')],
[InlineKeyboardButton("👥 Gestionar usuarios", callback_data='manage_users')],
@@ -26,7 +21,7 @@ def get_admin_menu():
return InlineKeyboardMarkup(keyboard)
def get_team_menu():
"""Creates and returns the main menu keyboard for the 'team' role."""
"""Crea el menú de botones para los Miembros del Equipo."""
keyboard = [
[InlineKeyboardButton("🕒 Proponer actividad", callback_data='propose_activity')],
[InlineKeyboardButton("📄 Ver estatus de solicitudes", callback_data='view_requests_status')],
@@ -34,7 +29,7 @@ def get_team_menu():
return InlineKeyboardMarkup(keyboard)
def get_client_menu():
"""Creates and returns the main menu keyboard for the 'client' role."""
"""Crea el menú de botones para los Clientes externos."""
keyboard = [
[InlineKeyboardButton("🗓️ Agendar una cita", callback_data='schedule_appointment')],
[InlineKeyboardButton(" Información de servicios", callback_data='get_service_info')],
@@ -43,20 +38,18 @@ def get_client_menu():
def handle_start(user_role):
"""
Handles the /start command by sending a role-based welcome message and menu.
This function acts as a router, determining which menu to display based on
the user's role, which is passed in as an argument.
Decide qué mensaje y qué menú mostrar según el rol del usuario.
"""
welcome_message = "Hola, soy Talía. ¿En qué puedo ayudarte hoy?"
# Dependiendo del rol, llamamos a una función de menú diferente
if user_role == "owner":
menu = get_owner_menu()
elif user_role == "admin":
menu = get_admin_menu()
elif user_role == "team":
menu = get_team_menu()
else: # Default to the client menu for any other role.
else: # Por defecto, si no es ninguno de los anteriores, es un cliente
menu = get_client_menu()
return welcome_message, menu

View File

@@ -1,11 +1,7 @@
# app/modules/print.py
"""
This module provides a command for administrators to print out the current
configuration details of the bot.
# Este módulo permite a los administradores imprimir los detalles de configuración del bot.
# Es una herramienta útil para depuración (debugging).
It is a debugging and administrative tool that allows authorized users to quickly
inspect key configuration variables without accessing the environment directly.
"""
from telegram import Update
from telegram.ext import ContextTypes
from ..permissions import is_admin
@@ -13,21 +9,22 @@ from ..config import TIMEZONE, CALENDAR_ID, N8N_WEBHOOK_URL
async def print_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Handles the /print command.
When triggered, this function first checks if the user has admin privileges.
If they do, it replies with a formatted message displaying the current values
of the TIMEZONE, CALENDAR_ID, and N8N_WEBHOOK_URL configuration variables.
If the user is not an admin, it sends a simple "not authorized" message.
Maneja el comando /print.
Verifica si el usuario es administrador. Si lo es, muestra valores clave
de la configuración (Zona horaria, ID de calendario, Webhook).
"""
chat_id = update.effective_chat.id
# Solo permitimos esto a los administradores
if is_admin(chat_id):
config_details = (
f"**Configuration Details**\n"
f"Timezone: `{TIMEZONE}`\n"
f"Calendar ID: `{CALENDAR_ID}`\n"
f"n8n Webhook URL: `{N8N_WEBHOOK_URL}`\n"
f"**Detalles de Configuración**\n"
f"Zona Horaria: `{TIMEZONE}`\n"
f"ID de Calendario: `{CALENDAR_ID}`\n"
f"URL Webhook n8n: `{N8N_WEBHOOK_URL}`\n"
)
await update.message.reply_text(config_details, parse_mode='Markdown')
else:
await update.message.reply_text("You are not authorized to use this command.")
# Si no es admin, le avisamos que no tiene permiso
await update.message.reply_text("No tienes autorización para usar este comando.")

View File

@@ -1,20 +1,13 @@
# app/modules/servicios.py
"""
This module is responsible for providing information about the services offered.
It's a simple informational module that gives clients an overview of the
available services and can be expanded to provide more detailed information
or initiate a quoting process.
"""
# Este módulo se encarga de dar información sobre los servicios ofrecidos.
# Es un módulo informativo para los clientes.
def get_service_info():
"""
Provides a brief overview of the available services.
Currently, this function returns a hardcoded list of services. For a more
dynamic and easily maintainable system, this information could be fetched
from a database, a configuration file, or an external API.
Muestra una lista breve de los servicios disponibles.
Por ahora devuelve un texto fijo. Se podría conectar a una base de datos
para que sea más fácil de actualizar.
"""
# TODO: Fetch service details from a database or a configuration file to
# make the service list easier to manage and update.
# TODO: Obtener detalles de servicios desde una base de datos o archivo de configuración.
return "Ofrecemos una variedad de servicios, incluyendo:\n\n- Consultoría Estratégica\n- Desarrollo de Software\n- Talleres de Capacitación\n\n¿Sobre cuál te gustaría saber más?"