feat: Restructure admin menu and enhance Vikunja integration

Restructures the admin menu into a primary and secondary menu for better user experience.
The primary menu now shows the most common actions.
The secondary menu contains less frequent admin commands.

Refactors the Vikunja module to be triggered by a menu button instead of a command.
Adds "edit task" functionality to the Vikunja module.
Fixes a bug where the button dispatcher was calling a non-existent function.
This commit is contained in:
google-labs-jules[bot]
2025-12-18 15:05:54 +00:00
parent f15b4661d2
commit c9ef9ab5b5
3 changed files with 133 additions and 72 deletions

View File

@@ -13,14 +13,26 @@ def get_owner_menu():
return InlineKeyboardMarkup(keyboard)
def get_admin_menu():
"""Crea el menú de botones para los Administradores."""
"""Crea el menú de botones principal para los Administradores."""
keyboard = [
[InlineKeyboardButton("📋 Ver Tareas (Vikunja)", callback_data='view_tasks')],
[InlineKeyboardButton("🏷️ Crear Tag NFC", callback_data='start_create_tag')],
[InlineKeyboardButton("📊 Estado del sistema", callback_data='view_system_status')],
[InlineKeyboardButton("⏳ Revisar Pendientes", callback_data='view_pending')],
[InlineKeyboardButton("📅 Agenda", callback_data='view_agenda')],
[InlineKeyboardButton("🏷️ Crear Tag", callback_data='start_create_tag')],
[InlineKeyboardButton("▶️ Más opciones", callback_data='admin_menu')],
]
return InlineKeyboardMarkup(keyboard)
def get_admin_secondary_menu():
"""Crea el menú secundario para Administradores."""
text = "Aquí tienes más opciones de administración:"
keyboard = [
[InlineKeyboardButton("📋 Gestionar Tareas (Vikunja)", callback_data='manage_vikunja')],
[InlineKeyboardButton("📊 Estado del sistema", callback_data='view_system_status')],
[InlineKeyboardButton("👥 Gestionar Usuarios", callback_data='manage_users')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
return text, reply_markup
def get_team_menu():
"""Crea el menú de botones para los Miembros del Equipo."""
keyboard = [
@@ -43,14 +55,13 @@ def handle_start(user_role):
"""
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: # Por defecto, si no es ninguno de los anteriores, es un cliente
else:
menu = get_client_menu()
return welcome_message, menu

View File

@@ -19,8 +19,8 @@ from app.permissions import is_admin
# Configuración del logger
logger = logging.getLogger(__name__)
# Definición de los estados de la conversación
SELECTING_ACTION, ADDING_TASK = range(2)
# Definición de los estados de la conversación para añadir y editar tareas
SELECTING_ACTION, ADDING_TASK, SELECTING_TASK_TO_EDIT, EDITING_TASK = range(4)
def get_vikunja_headers():
"""Devuelve los headers necesarios para la API de Vikunja."""
@@ -29,29 +29,13 @@ def get_vikunja_headers():
"Content-Type": "application/json",
}
async def vik_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Inicia la conversación de Vikunja y muestra el menú de acciones."""
if not is_admin(update.effective_chat.id):
await update.message.reply_text("No tienes permiso para usar este comando.")
return ConversationHandler.END
keyboard = [
[InlineKeyboardButton("Ver Tareas", callback_data='view_tasks')],
[InlineKeyboardButton("Añadir Tarea", callback_data='add_task')],
[InlineKeyboardButton("Cancelar", callback_data='cancel')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("Selecciona una opción para Vikunja:", reply_markup=reply_markup)
return SELECTING_ACTION
async def view_tasks(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Muestra la lista de tareas de Vikunja."""
query = update.callback_query
await query.answer()
def get_tasks():
"""
Obtiene y formatea la lista de tareas de Vikunja.
Esta función es síncrona y devuelve un string.
"""
if not VIKUNJA_API_TOKEN:
await query.edit_message_text("Error: VIKUNJA_API_TOKEN no configurado.")
return ConversationHandler.END
return "Error: VIKUNJA_API_TOKEN no configurado."
try:
response = requests.get(f"{VIKUNJA_API_URL}/projects/1/tasks", headers=get_vikunja_headers())
@@ -59,19 +43,32 @@ async def view_tasks(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
tasks = response.json()
if not tasks:
text = "No tienes tareas pendientes en Vikunja."
else:
text = "📋 *Tus Tareas en Vikunja*\n\n"
for task in tasks[:10]:
status = "" if task.get('done') else ""
text += f"{status} *{task.get('title')}*\n"
return "No tienes tareas pendientes en Vikunja."
await query.edit_message_text(text, parse_mode='Markdown')
text = "📋 *Tus Tareas en Vikunja*\n\n"
for task in sorted(tasks, key=lambda t: t.get('id', 0))[:10]:
status = "" if task.get('done') else ""
text += f"{status} `{task.get('id')}`: *{task.get('title')}*\n"
return text
except Exception as e:
logger.error(f"Error al obtener tareas de Vikunja: {e}")
await query.edit_message_text(f"Error al conectar con Vikunja: {e}")
return f"Error al conectar con Vikunja: {e}"
return ConversationHandler.END
async def vikunja_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Muestra el menú principal de acciones de Vikunja."""
query = update.callback_query
await query.answer()
keyboard = [
[InlineKeyboardButton("Añadir Tarea", callback_data='add_task')],
[InlineKeyboardButton("Editar Tarea", callback_data='edit_task_start')],
[InlineKeyboardButton("Volver", callback_data='cancel')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
tasks_list = get_tasks()
await query.edit_message_text(text=f"{tasks_list}\n\nSelecciona una acción:", reply_markup=reply_markup, parse_mode='Markdown')
return SELECTING_ACTION
async def request_task_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Solicita al usuario el título de la nueva tarea."""
@@ -83,13 +80,7 @@ async def request_task_title(update: Update, context: ContextTypes.DEFAULT_TYPE)
async def add_task(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Añade una nueva tarea a Vikunja."""
task_title = update.message.text
if not VIKUNJA_API_TOKEN:
await update.message.reply_text("Error: VIKUNJA_API_TOKEN no configurado.")
return ConversationHandler.END
try:
# Usamos un project_id=1 hardcodeado como se definió en el plan
data = {"title": task_title, "project_id": 1}
response = requests.post(f"{VIKUNJA_API_URL}/tasks", headers=get_vikunja_headers(), json=data)
response.raise_for_status()
@@ -100,8 +91,71 @@ async def add_task(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
return ConversationHandler.END
async def select_task_to_edit(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Muestra los botones para seleccionar qué tarea editar."""
query = update.callback_query
await query.answer()
try:
response = requests.get(f"{VIKUNJA_API_URL}/projects/1/tasks", headers=get_vikunja_headers())
response.raise_for_status()
tasks = [task for task in response.json() if not task.get('done')]
if not tasks:
await query.edit_message_text("No hay tareas pendientes para editar.")
return ConversationHandler.END
keyboard = []
for task in sorted(tasks, key=lambda t: t.get('id', 0))[:10]:
keyboard.append([InlineKeyboardButton(
f"{task.get('id')}: {task.get('title')}",
callback_data=f"edit_task:{task.get('id')}"
)])
keyboard.append([InlineKeyboardButton("Cancelar", callback_data='cancel')])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Selecciona la tarea que quieres editar:", reply_markup=reply_markup)
return SELECTING_TASK_TO_EDIT
except Exception as e:
logger.error(f"Error al obtener tareas para editar: {e}")
await query.edit_message_text("Error al obtener la lista de tareas.")
return ConversationHandler.END
async def request_new_task_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Solicita el nuevo título para la tarea seleccionada."""
query = update.callback_query
await query.answer()
task_id = query.data.split(':')[1]
context.user_data['task_id_to_edit'] = task_id
await query.edit_message_text(f"Introduce el nuevo título para la tarea `{task_id}`:", parse_mode='Markdown')
return EDITING_TASK
async def edit_task(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Actualiza el título de una tarea en Vikunja."""
new_title = update.message.text
task_id = context.user_data.get('task_id_to_edit')
if not task_id:
await update.message.reply_text("Error: No se encontró el ID de la tarea a editar.")
return ConversationHandler.END
try:
data = {"title": new_title}
response = requests.put(f"{VIKUNJA_API_URL}/tasks/{task_id}", headers=get_vikunja_headers(), json=data)
response.raise_for_status()
await update.message.reply_text(f"✅ Tarea `{task_id}` actualizada a *{new_title}*", parse_mode='Markdown')
except Exception as e:
logger.error(f"Error al editar la tarea {task_id}: {e}")
await update.message.reply_text("Error al actualizar la tarea.")
finally:
del context.user_data['task_id_to_edit']
return ConversationHandler.END
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Cancela la conversación."""
"""Cancela la conversación actual."""
query = update.callback_query
await query.answer()
await query.edit_message_text("Operación cancelada.")
@@ -110,16 +164,19 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
def vikunja_conv_handler():
"""Crea el ConversationHandler para el flujo de Vikunja."""
return ConversationHandler(
entry_points=[CommandHandler('vik', vik_start)],
entry_points=[CallbackQueryHandler(vikunja_menu, pattern='^manage_vikunja$')],
states={
SELECTING_ACTION: [
CallbackQueryHandler(view_tasks, pattern='^view_tasks$'),
CallbackQueryHandler(request_task_title, pattern='^add_task$'),
CallbackQueryHandler(select_task_to_edit, pattern='^edit_task_start$'),
CallbackQueryHandler(cancel, pattern='^cancel$'),
],
ADDING_TASK: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_task)
ADDING_TASK: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_task)],
SELECTING_TASK_TO_EDIT: [
CallbackQueryHandler(request_new_task_title, pattern=r'^edit_task:\d+$'),
CallbackQueryHandler(cancel, pattern='^cancel$'),
],
EDITING_TASK: [MessageHandler(filters.TEXT & ~filters.COMMAND, edit_task)],
},
fallbacks=[CommandHandler('cancel', cancel)],
)