🏆 Ranking del Mes (Tubos)
- Cargando...
from flask import Flask, request, jsonify, render_template_string import os import logging from dotenv import load_dotenv from datetime import datetime import json # Cargar variables de entorno load_dotenv() # Importar módulos personalizados from mattermost_client import MattermostClient from nocodb_client import NocoDBClient from handlers import handle_venta_message, generar_reporte_diario from utils import validar_token_outgoing from websocket_listener import MattermostWebsocketListener # Configurar logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # Inicializar Flask app = Flask(__name__) # Inicializar clientes mattermost = MattermostClient( url=os.getenv('MATTERMOST_URL'), token=os.getenv('MATTERMOST_BOT_TOKEN') ) nocodb = NocoDBClient( url=os.getenv('NOCODB_URL'), token=os.getenv('NOCODB_TOKEN') ) # Inicializar websocket listener ws_listener = MattermostWebsocketListener(mattermost, nocodb, handle_venta_message) ws_listener.start() logger.info("Websocket listener iniciado para escuchar mensajes de Mattermost") @app.route('/health', methods=['GET']) def health_check(): """Endpoint para verificar que el bot está funcionando""" return jsonify({ 'status': 'healthy', 'timestamp': datetime.now().isoformat(), 'version': '1.0.0' }), 200 @app.route('/webhook/mattermost', methods=['POST']) def mattermost_webhook(): """ Recibe webhooks salientes de Mattermost cuando hay mensajes en el canal de ventas """ try: data = request.json logger.info(f"Webhook recibido: {json.dumps(data, indent=2)}") # Validar token token = data.get('token') if not validar_token_outgoing(token): logger.warning(f"Token inválido: {token}") return jsonify({'error': 'Token inválido'}), 403 # Ignorar mensajes del propio bot if data.get('user_name') == 'sales-bot': return jsonify({'status': 'ignored', 'reason': 'bot message'}), 200 # Obtener información del mensaje channel_name = data.get('channel_name') user_name = data.get('user_name') text = data.get('text', '').strip() logger.info(f"Procesando mensaje de {user_name} en #{channel_name}: {text}") # Verificar si es un mensaje de venta palabras_clave = ['venta', 'vendi', 'vendí', 'cliente', 'ticket'] es_venta = any(palabra in text.lower() for palabra in palabras_clave) if es_venta or data.get('file_ids'): respuesta = handle_venta_message(data, mattermost, nocodb) return jsonify(respuesta), 200 return jsonify({'status': 'ok', 'message': 'Mensaje procesado'}), 200 except Exception as e: logger.error(f"Error procesando webhook: {str(e)}", exc_info=True) return jsonify({'error': 'Error interno del servidor'}), 500 @app.route('/webhook/nocodb', methods=['POST']) def nocodb_webhook(): """ Recibe webhooks de NocoDB cuando se insertan/actualizan datos """ try: data = request.json logger.info(f"Webhook NocoDB recibido: {json.dumps(data, indent=2)}") # Aquí puedes agregar lógica para notificar en Mattermost # cuando se actualicen datos en NocoDB return jsonify({'status': 'ok'}), 200 except Exception as e: logger.error(f"Error procesando webhook NocoDB: {str(e)}", exc_info=True) return jsonify({'error': 'Error interno del servidor'}), 500 @app.route('/comando/metas', methods=['POST']) def comando_metas(): """ Endpoint para el comando slash /metas en Mattermost """ try: data = request.form.to_dict() logger.info(f"Comando /metas recibido de {data.get('user_name')}") # Validar token token = data.get('token') expected_tokens = [ os.getenv('MATTERMOST_SLASH_TOKEN_METAS'), os.getenv('MATTERMOST_OUTGOING_TOKEN') ] if token not in expected_tokens: return jsonify({'text': 'Token inválido'}), 403 user_name = data.get('user_name') # Obtener meta del vendedor meta = nocodb.get_meta_vendedor(user_name) if not meta: mensaje = ( f"@{user_name} Aún no tienes ventas registradas este mes.\n" "¡Empieza a vender y registra tus ventas!" ) else: porcentaje = meta.get('porcentaje_completado', 0) total_vendido = meta.get('total_vendido', 0) meta_establecida = meta.get('meta_establecida', 0) ventas_count = meta.get('ventas_realizadas', 0) falta = meta_establecida - total_vendido # Barra de progreso visual barra_length = 20 completado = int((porcentaje / 100) * barra_length) barra = '█' * completado + '░' * (barra_length - completado) mensaje = ( f"📊 **Reporte de {user_name}**\n\n" f"`{barra}` {porcentaje:.1f}%\n\n" f"**Total vendido:** ${total_vendido:,.2f} MXN\n" f"**Meta mensual:** ${meta_establecida:,.2f} MXN\n" f"**Falta:** ${falta:,.2f} MXN\n" f"**Ventas realizadas:** {ventas_count}\n" ) if porcentaje >= 100: mensaje += "\n🎉 **¡Felicidades! Meta completada**" elif porcentaje >= 75: mensaje += f"\n🔥 **¡Casi llegas! Solo faltan ${falta:,.2f}**" return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /metas: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/ranking', methods=['POST']) def comando_ranking(): """ Endpoint para el comando slash /ranking en Mattermost """ try: data = request.form.to_dict() logger.info(f"Comando /ranking recibido de {data.get('user_name')}") # Validar token token = data.get('token') expected_tokens = [ os.getenv('MATTERMOST_SLASH_TOKEN_RANKING'), os.getenv('MATTERMOST_OUTGOING_TOKEN') ] if token not in expected_tokens: return jsonify({'text': 'Token inválido'}), 403 # Obtener ranking ranking = nocodb.get_ranking_vendedores() if not ranking: mensaje = "No hay datos de ventas este mes." else: mensaje = "🏆 **Ranking de Vendedores - Mes Actual**\n\n" for i, vendedor in enumerate(ranking[:10], 1): username = vendedor.get('vendedor_username') total = vendedor.get('total_vendido', 0) porcentaje = vendedor.get('porcentaje_completado', 0) ventas = vendedor.get('ventas_realizadas', 0) # Medallas para top 3 if i == 1: emoji = '🥇' elif i == 2: emoji = '🥈' elif i == 3: emoji = '🥉' else: emoji = f'{i}.' mensaje += ( f"{emoji} **@{username}**\n" f" └ ${total:,.2f} MXN ({porcentaje:.1f}%) - {ventas} ventas\n" ) return jsonify({ 'response_type': 'in_channel', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /ranking: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/ayuda', methods=['POST']) def comando_ayuda(): """ Endpoint para el comando slash /ayuda en Mattermost """ try: data = request.form.to_dict() logger.info(f"Comando /ayuda recibido de {data.get('user_name')}") # Validar token token = data.get('token') expected_tokens = [ os.getenv('MATTERMOST_SLASH_TOKEN_AYUDA'), os.getenv('MATTERMOST_OUTGOING_TOKEN') ] if token not in expected_tokens: return jsonify({'text': 'Token inválido'}), 403 mensaje = ( "🤖 **Bot de Ventas - Guía de Uso**\n\n" "**Para registrar una venta:**\n" "• `venta @monto 1500 @cliente Juan Pérez`\n" "• `vendí $1500 a María García`\n" "• También puedes adjuntar foto del ticket\n\n" "**Comandos disponibles:**\n" "• `/metas` - Ver tu progreso del mes\n" "• `/ranking` - Ver ranking de vendedores\n" "• `/ayuda` - Mostrar esta ayuda\n\n" "**Ejemplos de registro de ventas:**\n" "✅ `venta @monto 2500 @cliente Empresa ABC`\n" "✅ `vendí $1,200.50 a cliente Pedro`\n" "✅ `venta @monto 5000 @cliente Tienda XYZ`\n\n" "**Consejos:**\n" "• Registra tus ventas inmediatamente después de cerrarlas\n" "• Incluye el nombre del cliente para mejor seguimiento\n" "• Revisa tu progreso regularmente con `/metas`\n" "• Compite sanamente con tus compañeros en el `/ranking`\n\n" "¡Sigue adelante! 💪" ) return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /ayuda: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/reporte/diario', methods=['POST']) def reporte_diario_manual(): """ Endpoint para generar reporte diario manualmente """ try: resultado = generar_reporte_diario(mattermost, nocodb) return jsonify(resultado), 200 except Exception as e: logger.error(f"Error generando reporte: {str(e)}", exc_info=True) return jsonify({'error': str(e)}), 500 @app.route('/test/mattermost', methods=['GET']) def test_mattermost(): """Test de conexión con Mattermost""" try: resultado = mattermost.test_connection() return jsonify(resultado), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/test/nocodb', methods=['GET']) def test_nocodb(): """Test de conexión con NocoDB""" try: resultado = nocodb.test_connection() return jsonify(resultado), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # ============== DASHBOARD ============== @app.route('/api/dashboard/resumen', methods=['GET']) def api_dashboard_resumen(): """API: Resumen general del día y mes""" try: from datetime import datetime, timedelta, timezone TZ_MEXICO = timezone(timedelta(hours=-6)) hoy = datetime.now(TZ_MEXICO).strftime('%Y-%m-%d') mes = datetime.now(TZ_MEXICO).strftime('%Y-%m') # Ventas del día ventas_hoy = nocodb.get_ventas_dia() total_hoy = sum(float(v.get('monto', 0)) for v in ventas_hoy) # Ventas del mes ventas_mes = nocodb.get_ventas_mes() total_mes = sum(float(v.get('monto', 0)) for v in ventas_mes) # Contar vendedores activos hoy vendedores_hoy = set(v.get('vendedor_username') for v in ventas_hoy) return jsonify({ 'fecha': hoy, 'mes': mes, 'ventas_hoy': len(ventas_hoy), 'monto_hoy': total_hoy, 'ventas_mes': len(ventas_mes), 'monto_mes': total_mes, 'vendedores_activos_hoy': len(vendedores_hoy) }), 200 except Exception as e: logger.error(f"Error en API resumen: {str(e)}") return jsonify({'error': str(e)}), 500 @app.route('/api/dashboard/ranking', methods=['GET']) def api_dashboard_ranking(): """API: Ranking de vendedores del mes""" try: ranking = nocodb.get_ranking_vendedores() return jsonify(ranking), 200 except Exception as e: logger.error(f"Error en API ranking: {str(e)}") return jsonify({'error': str(e)}), 500 @app.route('/api/dashboard/ventas-recientes', methods=['GET']) def api_dashboard_ventas_recientes(): """API: Últimas ventas registradas con nombres de vendedores""" try: import requests ventas = nocodb.get_ventas_dia() # Obtener lista de vendedores para mapear usernames a nombres vendedores_response = requests.get( f"{nocodb.url}/api/v2/tables/{nocodb.table_vendedores}/records", headers=nocodb.headers, params={'limit': 100}, timeout=10 ) vendedores_response.raise_for_status() vendedores = vendedores_response.json().get('list', []) # Crear mapa de username -> nombre_completo nombres_map = {v.get('username'): v.get('nombre_completo', v.get('username')) for v in vendedores} # Agregar nombre_completo a cada venta for venta in ventas: username = venta.get('vendedor_username', '') venta['nombre_completo'] = nombres_map.get(username, username) # Ordenar por fecha descendente y tomar las últimas 20 ventas_ordenadas = sorted(ventas, key=lambda x: x.get('fecha_venta', ''), reverse=True)[:20] return jsonify(ventas_ordenadas), 200 except Exception as e: logger.error(f"Error en API ventas recientes: {str(e)}") return jsonify({'error': str(e)}), 500 @app.route('/api/dashboard/metas', methods=['GET']) def api_dashboard_metas(): """API: Estado de metas de todos los vendedores""" try: import requests response = requests.get( f"{nocodb.url}/api/v2/tables/{nocodb.table_metas}/records", headers=nocodb.headers, params={'limit': 100}, timeout=10 ) response.raise_for_status() metas = response.json().get('list', []) return jsonify(metas), 200 except Exception as e: logger.error(f"Error en API metas: {str(e)}") return jsonify({'error': str(e)}), 500 @app.route('/dashboard') def dashboard(): """Dashboard principal de ventas""" html = '''