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 from scheduler import SalesBotScheduler # 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") # Inicializar scheduler de tareas programadas scheduler = SalesBotScheduler(mattermost, nocodb) scheduler.iniciar() logger.info("Scheduler de tareas programadas iniciado") @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(s) del ticket\n" "• _Todas las ventas requieren confirmación_\n\n" "**Comandos básicos:**\n" "• `/metas` - Ver tu progreso del mes\n" "• `/ranking` - Ver ranking de vendedores\n" "• `/ayuda` - Mostrar esta ayuda\n\n" "**Comandos de gestión:**\n" "• `/cancelar [motivo]` - Cancelar una venta\n" "• `/editar @monto X @cliente Y` - Editar una venta\n" "• `/deshacer` - Deshacer última venta (5 min)\n\n" "**Comandos de comisiones:**\n" "• `/comisiones` - Ver historial de comisiones\n" "• `/racha` - Ver tu racha actual y bonos\n" "• `/exportar [csv] [mes]` - Exportar ventas a Excel/CSV\n\n" "**Sistema de confirmación:**\n" "1. Envía tu mensaje de venta o foto\n" "2. El bot muestra un preview\n" "3. Responde **si** para confirmar o **no** para cancelar\n\n" "**Bonos por racha:**\n" "• 3 días consecutivos: +$20\n" "• 5 días consecutivos: +$50\n" "• 10 días consecutivos: +$150\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 # ============== NUEVOS COMANDOS FASE 1-3 ============== @app.route('/comando/cancelar', methods=['POST']) def comando_cancelar(): """ Endpoint para el comando slash /cancelar en Mattermost Uso: /cancelar [motivo] """ try: from utils import extraer_id_venta, extraer_motivo, validar_tokens_comando data = request.form.to_dict() logger.info(f"Comando /cancelar recibido de {data.get('user_name')}") # Validar token token = data.get('token') if not validar_tokens_comando(token, 'cancelar'): return jsonify({'text': '❌ Token inválido'}), 403 user_name = data.get('user_name') texto = data.get('text', '').strip() # Extraer ID de venta venta_id = extraer_id_venta(texto) if not venta_id: return jsonify({ 'response_type': 'ephemeral', 'text': '❌ **Uso incorrecto**\n\nFormato: `/cancelar [motivo]`\n\nEjemplo: `/cancelar 123 "cliente canceló"`' }), 200 # Extraer motivo opcional motivo = extraer_motivo(texto) # Cancelar venta resultado = nocodb.cancelar_venta(venta_id, user_name, motivo) if resultado.get('success'): mensaje = ( f"✅ **Venta #{venta_id} cancelada**\n\n" f"**Monto:** ${resultado.get('monto', 0):,.2f}\n" f"**Cliente:** {resultado.get('cliente', 'N/A')}\n" ) if motivo: mensaje += f"**Motivo:** {motivo}\n" mensaje += "\n_Las metas han sido recalculadas._" else: mensaje = f"❌ **Error:** {resultado.get('error')}" return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /cancelar: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/deshacer', methods=['POST']) def comando_deshacer(): """ Endpoint para el comando slash /deshacer en Mattermost Cancela la última venta del usuario (dentro de los últimos 5 minutos) """ try: from utils import validar_tokens_comando data = request.form.to_dict() logger.info(f"Comando /deshacer recibido de {data.get('user_name')}") # Validar token token = data.get('token') if not validar_tokens_comando(token, 'deshacer'): return jsonify({'text': '❌ Token inválido'}), 403 user_name = data.get('user_name') # Obtener última venta ultima_venta = nocodb.get_ultima_venta_usuario(user_name, minutos=5) if not ultima_venta: return jsonify({ 'response_type': 'ephemeral', 'text': '❌ **No hay ventas recientes para deshacer**\n\nSolo puedes deshacer ventas de los últimos 5 minutos.' }), 200 venta_id = ultima_venta.get('Id') # Cancelar la venta resultado = nocodb.cancelar_venta(venta_id, user_name, 'Deshecho por usuario') if resultado.get('success'): mensaje = ( f"↩️ **Venta #{venta_id} deshecha**\n\n" f"**Monto:** ${resultado.get('monto', 0):,.2f}\n" f"**Cliente:** {resultado.get('cliente', 'N/A')}\n\n" "_Las metas han sido recalculadas._" ) else: mensaje = f"❌ **Error:** {resultado.get('error')}" return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /deshacer: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/editar', methods=['POST']) def comando_editar(): """ Endpoint para el comando slash /editar en Mattermost Uso: /editar @monto 1600 @cliente Nuevo Cliente """ try: from utils import extraer_id_venta, extraer_monto, extraer_cliente, validar_tokens_comando data = request.form.to_dict() logger.info(f"Comando /editar recibido de {data.get('user_name')}") # Validar token token = data.get('token') if not validar_tokens_comando(token, 'editar'): return jsonify({'text': '❌ Token inválido'}), 403 user_name = data.get('user_name') texto = data.get('text', '').strip() # Extraer ID de venta venta_id = extraer_id_venta(texto) if not venta_id: return jsonify({ 'response_type': 'ephemeral', 'text': '❌ **Uso incorrecto**\n\nFormato: `/editar @monto 1600 @cliente NuevoCliente`\n\nEjemplo: `/editar 123 @monto 2000`' }), 200 # Extraer campos a actualizar campos = {} nuevo_monto = extraer_monto(texto) nuevo_cliente = extraer_cliente(texto) if nuevo_monto: campos['monto'] = nuevo_monto if nuevo_cliente: campos['cliente'] = nuevo_cliente if not campos: return jsonify({ 'response_type': 'ephemeral', 'text': '❌ **No se especificaron cambios**\n\nUsa `@monto` y/o `@cliente` para especificar los cambios.' }), 200 # Editar venta resultado = nocodb.editar_venta(venta_id, user_name, campos) if resultado.get('success'): cambios_texto = '\n'.join([ f"• **{c['campo']}:** {c['anterior']} → {c['nuevo']}" for c in resultado.get('cambios', []) ]) mensaje = ( f"✏️ **Venta #{venta_id} editada**\n\n" f"**Cambios realizados:**\n{cambios_texto}\n\n" "_Las metas han sido recalculadas._" ) else: mensaje = f"❌ **Error:** {resultado.get('error')}" return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /editar: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/comisiones', methods=['POST']) def comando_comisiones(): """ Endpoint para el comando slash /comisiones en Mattermost Muestra historial de comisiones de los últimos meses """ try: from utils import validar_tokens_comando data = request.form.to_dict() logger.info(f"Comando /comisiones recibido de {data.get('user_name')}") # Validar token token = data.get('token') if not validar_tokens_comando(token, 'comisiones'): return jsonify({'text': '❌ Token inválido'}), 403 user_name = data.get('user_name') # Obtener historial historial = nocodb.get_historial_comisiones(user_name, meses=6) if not historial: mensaje = f"📊 **Historial de Comisiones - @{user_name}**\n\nNo hay datos de comisiones." else: mensaje = f"📊 **Historial de Comisiones - @{user_name}**\n\n" mensaje += "| Mes | Tubos | Comisión | Ventas |\n" mensaje += "|-----|-------|----------|--------|\n" total_comision = 0 for h in historial: mensaje += f"| {h['mes']} | {h['tubos_totales']} | ${h['comision_total']:,.2f} | {h['cantidad_ventas']} |\n" total_comision += h['comision_total'] mensaje += f"\n**Total acumulado:** ${total_comision:,.2f} 💰" return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /comisiones: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/racha', methods=['POST']) def comando_racha(): """ Endpoint para el comando slash /racha en Mattermost Muestra la racha actual del vendedor """ try: from utils import validar_tokens_comando data = request.form.to_dict() logger.info(f"Comando /racha recibido de {data.get('user_name')}") # Validar token token = data.get('token') if not validar_tokens_comando(token, 'racha'): return jsonify({'text': '❌ Token inválido'}), 403 user_name = data.get('user_name') # Obtener racha racha = nocodb.verificar_racha(user_name) dias = racha.get('dias_consecutivos', 0) bonus = racha.get('bonus', 0) proximo = racha.get('proximo_bonus', {}) # Determinar emoji según racha if dias >= 10: emoji = '🔥🔥🔥' nivel = 'LEGENDARIO' elif dias >= 5: emoji = '🔥🔥' nivel = 'EN FUEGO' elif dias >= 3: emoji = '🔥' nivel = 'RACHA ACTIVA' else: emoji = '💪' nivel = 'CONSTRUYENDO' mensaje = f"{emoji} **Racha de @{user_name}** {emoji}\n\n" mensaje += f"**Nivel:** {nivel}\n" mensaje += f"**Días consecutivos:** {dias}\n" mensaje += f"**Meta diaria:** {racha.get('meta_diaria', 3)} tubos\n\n" if bonus > 0: mensaje += f"**💰 Bonus ganado:** ${bonus:,.2f}\n\n" if proximo.get('dias_faltan', 0) > 0: mensaje += f"**Próximo bonus:** ${proximo['bonus']:,.2f} en {proximo['dias_faltan']} día(s)\n" elif proximo.get('mensaje'): mensaje += f"**{proximo['mensaje']}**\n" mensaje += "\n_¡Sigue cumpliendo tu meta diaria para mantener la racha!_" return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except Exception as e: logger.error(f"Error procesando comando /racha: {str(e)}", exc_info=True) return jsonify({ 'text': f'❌ Error procesando comando: {str(e)}' }), 500 @app.route('/comando/exportar', methods=['POST']) def comando_exportar(): """ Endpoint para el comando slash /exportar en Mattermost Exporta ventas a Excel o CSV """ try: from utils import validar_tokens_comando, parsear_formato_exportar from export_utils import generar_excel_ventas, generar_csv_ventas data = request.form.to_dict() logger.info(f"Comando /exportar recibido de {data.get('user_name')}") # Validar token token = data.get('token') if not validar_tokens_comando(token, 'exportar'): return jsonify({'text': '❌ Token inválido'}), 403 user_name = data.get('user_name') texto = data.get('text', '').strip() channel_id = data.get('channel_id') # Parsear parámetros formato, mes = parsear_formato_exportar(texto) if not mes: mes = datetime.now().strftime('%Y-%m') # Obtener datos ventas = nocodb.get_ventas_mes(user_name, mes) stats = nocodb.get_estadisticas_vendedor_mes(user_name, mes) if not ventas: return jsonify({ 'response_type': 'ephemeral', 'text': f'📊 No hay ventas para {user_name} en {mes}' }), 200 # Generar archivo if formato == 'excel': contenido = generar_excel_ventas(ventas, user_name, stats) filename = f'ventas_{user_name}_{mes}.xlsx' else: contenido = generar_csv_ventas(ventas) filename = f'ventas_{user_name}_{mes}.csv' # Subir a Mattermost file_response = mattermost.upload_file(channel_id, contenido, filename) if file_response: mensaje = f"📊 **Exportación completada**\n\nArchivo: `{filename}`\n\n_El archivo ha sido subido al canal._" else: mensaje = f"❌ Error al subir el archivo. Intenta de nuevo." return jsonify({ 'response_type': 'ephemeral', 'text': mensaje }), 200 except ImportError: return jsonify({ 'response_type': 'ephemeral', 'text': '❌ Módulo de exportación no disponible. Contacta al administrador.' }), 200 except Exception as e: logger.error(f"Error procesando comando /exportar: {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 = ''' Sales Bot - Dashboard

Sales Bot Dashboard

Ventas Hoy
-
$0.00
Ventas del Mes
-
$0.00
Vendedores Activos Hoy
-
Meta Diaria
3
tubos por vendedor

🏆 Ranking del Mes (Tubos)

  • Cargando...

📋 Ventas Recientes

Cargando...
''' return render_template_string(html) if __name__ == '__main__': port = int(os.getenv('FLASK_PORT', 5000)) host = os.getenv('FLASK_HOST', '0.0.0.0') debug = os.getenv('DEBUG', 'False').lower() == 'true' logger.info(f"Iniciando Sales Bot en {host}:{port}") app.run(host=host, port=port, debug=debug)