Files
whatsapp/index.php
T
2026-02-20 09:07:37 -05:00

2159 lines
114 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Página principal del sistema WhatsApp Bot
* Desarrollado por U-Site.app
*/
require_once 'config/config.php';
// Verificar autenticación - redirigir al login si no está logueado
if (!isUserLoggedIn()) {
header('Location: login.php');
exit('Acceso denegado. <a href="login.php">Iniciar sesión</a>');
}
// Verificar conexión a la base de datos
try {
$db = Database::getInstance();
$dbStatus = 'Conectado';
} catch (Exception $e) {
$dbStatus = 'Error: ' . $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WhatsApp Bot Manager</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
<link href="./assets/css/diagnostic.css?v=12" rel="stylesheet">
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-header">
<h4><i class="fas fa-robot"></i> WhatsApp Bot</h4>
</div>
<ul class="sidebar-menu">
<li><a href="#dashboard" class="nav-link active" data-tab="dashboard"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
<li><a href="#conversations" class="nav-link" data-tab="conversations"><i class="fas fa-comments"></i> Conversaciones <span id="conversations-count" class="sidebar-badge badge bg-danger" style="display:none; font-size:0.72rem; padding:0.15rem 0.4rem;">0</span></a></li>
<li><a href="#users" class="nav-link" data-tab="users"><i class="fas fa-users"></i> Usuarios</a></li>
<li><a href="#menus" class="nav-link" data-tab="menus"><i class="fas fa-list"></i> Menús</a></li>
<li><a href="#admin_users" class="nav-link" data-tab="admin_users"><i class="fas fa-user-shield"></i> Usuarios del Sistema</a></li>
<li><a href="#send_message" class="nav-link" data-tab="send_message"><i class="fas fa-paper-plane"></i> Enviar Mensaje</a></li>
<li><a href="#broadcast" class="nav-link" data-tab="broadcast"><i class="fas fa-broadcast-tower"></i> Envío Masivo <span class="badge bg-success ms-1">NUEVO</span></a></li>
<li><a href="scheduled_messages.php" class="nav-link"><i class="fas fa-clock"></i> Mensajes Programados</a></li>
<li><a href="#templates" class="nav-link" data-tab="templates"><i class="fas fa-file-text"></i> Plantillas</a></li>
<li><a href="#autoresponses" class="nav-link" data-tab="autoresponses"><i class="fas fa-robot"></i> Respuestas Auto</a></li>
<li><a href="#system_config" class="nav-link" data-tab="system_config"><i class="fas fa-cog"></i> Configuración</a></li>
<li><a href="#logs" class="nav-link" data-tab="logs"><i class="fas fa-file-alt"></i> Logs</a></li>
<li class="sidebar-divider"></li>
<li><a href="logout.php" class="nav-link logout-link" onclick="return confirm('¿Está seguro que desea cerrar sesión?')"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</a></li>
</ul>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Notification toasts container (global) -->
<div id="global-notification-toasts" style="position:fixed; top:12px; right:12px; z-index:9999;"></div>
<!-- Header -->
<header class="content-header">
<div>
<h1><i class="fas fa-robot"></i> Sistema de Gestión WhatsApp Bot</h1>
<small class="text-muted">
<i class="fas fa-user"></i> Conectado como <strong><?= htmlspecialchars(
// Preferir nombre completo del admin si existe
(
$_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? ADMIN_USERNAME
)
) ?></strong>
| <i class="fas fa-clock"></i> Sesión desde: <?= date('H:i:s', $_SESSION['login_time'] ?? time()) ?>
</small>
</div>
<div class="header-actions">
<!-- Sidebar toggle (visible en pantallas pequeñas) -->
<button id="sidebar-toggle" class="btn btn-light d-md-none me-2" title="Abrir menú">
<i class="fas fa-bars"></i>
</button>
<div class="status-badge">
<span class="status-dot status-online"></span>
<span class="status-text">En línea</span>
</div> <!-- Notification bell -->
<div class="me-2">
<button id="notification-bell" class="btn btn-light position-relative" title="Notificaciones">
<i class="fas fa-bell"></i>
<span id="notification-count" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="display:none;">0</span>
</button>
</div> <button class="btn btn-primary me-2" onclick="refreshData()">
<i class="fas fa-sync-alt"></i> Actualizar
</button>
<a href="logout.php" class="btn btn-outline-danger" onclick="return confirm('¿Está seguro que desea cerrar sesión?')">
<i class="fas fa-sign-out-alt"></i> Cerrar Sesión
</a>
</div>
</header>
<!-- Dashboard Tab -->
<div id="dashboard" class="tab-content active">
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stat bg-primary text-white">
<div class="card-body">
<div class="row align-items-center">
<div class="col">
<h6 class="card-stat-title">Total Usuarios</h6>
<h3 class="card-stat-number" id="total-users">-</h3>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stat bg-success text-white">
<div class="card-body">
<div class="row align-items-center">
<div class="col">
<h6 class="card-stat-title">Mensajes Hoy</h6>
<h3 class="card-stat-number" id="conversations-today">-</h3>
</div>
<div class="col-auto">
<i class="fas fa-comment fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stat bg-info text-white">
<div class="card-body">
<div class="row align-items-center">
<div class="col">
<h6 class="card-stat-title">Usuarios Activos</h6>
<h3 class="card-stat-number" id="active-users">-</h3>
</div>
<div class="col-auto">
<i class="fas fa-user-check fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stat bg-warning text-white">
<div class="card-body">
<div class="row align-items-center">
<div class="col">
<h6 class="card-stat-title">Total Mensajes</h6>
<h3 class="card-stat-number" id="total-conversations">-</h3>
</div>
<div class="col-auto">
<i class="fas fa-envelope fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-chart-line"></i> Actividad Reciente</h5>
</div>
<div class="card-body">
<canvas id="conversationsChart" height="100"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-clock"></i> Últimos Mensajes</h5>
</div>
<div class="card-body">
<div id="recent-conversations" class="list-group">
<!-- Contenido dinámico -->
</div>
</div>
</div>
</div>
</div>
<!-- Rate Limiting Monitor -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-tachometer-alt"></i> Monitoreo de Rate Limiting - Facebook API</h5>
<button class="btn btn-sm btn-outline-primary" onclick="refreshRateLimitStats()">
<i class="fas fa-sync-alt"></i> Actualizar
</button>
</div>
<div class="card-body">
<!-- Status General -->
<div class="alert" id="rate-limit-status" role="alert">
<i class="fas fa-spinner fa-spin"></i> Cargando estadísticas...
</div>
<!-- Métricas de Facebook -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body text-center">
<h6 class="text-muted mb-2">Call Count</h6>
<div class="progress mb-2" style="height: 25px;">
<div id="fb-call-count-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
</div>
<small class="text-muted">Llamadas a la API</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body text-center">
<h6 class="text-muted mb-2">Total Time</h6>
<div class="progress mb-2" style="height: 25px;">
<div id="fb-total-time-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
</div>
<small class="text-muted">Tiempo de procesamiento</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body text-center">
<h6 class="text-muted mb-2">Total CPU Time</h6>
<div class="progress mb-2" style="height: 25px;">
<div id="fb-cpu-time-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
</div>
<small class="text-muted">Tiempo de CPU</small>
</div>
</div>
</div>
</div>
<!-- Límites Locales -->
<div class="row">
<div class="col-md-12">
<h6 class="mb-3"><i class="fas fa-shield-alt"></i> Límites Locales (Redis)</h6>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h6>Por Segundo</h6>
<h4 id="local-limit-second" class="mb-0">-</h4>
<small class="text-muted">Límite: <span id="local-limit-second-max">80</span>/s</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h6>Por Hora</h6>
<h4 id="local-limit-hour" class="mb-0">-</h4>
<small class="text-muted">Límite: <span id="local-limit-hour-max">1000</span>/h</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h6>Por Día</h6>
<h4 id="local-limit-day" class="mb-0">-</h4>
<small class="text-muted">Límite: <span id="local-limit-day-max">10000</span>/d</small>
</div>
</div>
</div>
</div>
<!-- Alertas Recientes -->
<div class="row mt-4" id="rate-limit-alerts-section" style="display:none;">
<div class="col-12">
<h6 class="mb-3"><i class="fas fa-exclamation-triangle"></i> Alertas Recientes</h6>
<div id="rate-limit-alerts" class="list-group">
<!-- Alertas dinámicas -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Conversations Tab -->
<div id="conversations" class="tab-content">
<div class="card conversation-card">
<div class="card-header bg-gradient-primary text-white">
<div class="row align-items-center">
<div class="col">
<h5 class="mb-0 d-flex align-items-center">
<i class="fas fa-comments"></i>&nbsp; Conversaciones
<a href="conversations.php" class="btn btn-sm btn-light ms-3" style="font-size:0.75rem;">Ver nueva versión</a>
</h5>
<small class="text-white-50">Administra tus chats de WhatsApp</small>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-light border-0">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control border-0"
placeholder="Buscar conversaciones..."
id="search-conversations">
</div>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="conversation-list" id="conversations-container">
<!-- Contenido dinámico -->
</div>
</div>
</div>
</div>
<!-- Users Tab -->
<div id="users" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-users"></i> Gestión de Usuarios</h5>
<button class="btn btn-primary" onclick="exportUsers()">
<i class="fas fa-download"></i> Exportar
</button>
</div>
<div class="card-body">
<!-- Búsqueda de usuarios -->
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input
type="text"
class="form-control"
id="users-search"
placeholder="Buscar por teléfono, nombre o email..."
onkeyup="searchUsers()"
>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Teléfono</th>
<th>Nombre</th>
<th>Estado</th>
<th>Menú Actual</th>
<th>Registro</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="users-table">
<!-- Contenido dinámico -->
</tbody>
</table>
</div>
<!-- Paginación -->
<div id="users-pagination"></div>
</div>
</div>
</div>
<!-- Menus Tab -->
<div id="menus" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-list"></i> Menús Configurados</h5>
<button class="btn btn-primary" onclick="openCreateMenuModal()">
<i class="fas fa-plus"></i> Nuevo Menú
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Nombre</th>
<th>Clave</th>
<th>Descripción</th>
<th>Estado</th>
<th>Opciones</th>
<th>Fecha Creación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="menus-table">
<!-- Contenido dinámico -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Admin Users Tab -->
<div id="admin_users" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-user-shield"></i> Usuarios del Sistema</h5>
<button class="btn btn-primary" onclick="showCreateAdminUserModal()">
<i class="fas fa-plus"></i> Nuevo Usuario
</button>
</div>
<div class="card-body">
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle"></i>
Gestiona los usuarios administradores que tienen acceso al sistema.
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Usuario</th>
<th>Nombre Completo</th>
<th>Email</th>
<th>Estado</th>
<th>Último Acceso</th>
<th>Fecha Creación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="admin-users-table">
<tr>
<td colspan="7" class="text-center">
<i class="fas fa-spinner fa-spin"></i> Cargando usuarios...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- conversations Tab -->
<div id="send_message" class="tab-content">
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-paper-plane"></i> Enviar Mensaje</h5>
<div>
<button class="btn btn-sm btn-secondary" onclick="whatsappManager.loadconversations()">🔄 Actualizar</button>
</div>
</div>
<div class="card-body">
<form id="send-message-form">
<div class="mb-3">
<label class="form-label">Tipo de Destinatario</label>
<select class="form-control" id="recipient-type" onchange="toggleRecipientType()">
<option value="existing">Usuario Registrado</option>
<option value="manual">Número Manual (Solo Plantillas)</option>
</select>
</div>
<div class="mb-3" id="existing-recipient-group">
<label class="form-label">Destinatario</label>
<select class="form-control" id="message-recipient">
<option value="">Seleccionar usuario...</option>
</select>
</div>
<div class="mb-3" id="manual-recipient-group" style="display: none;">
<label class="form-label">Número de WhatsApp</label>
<input type="text" class="form-control" id="manual-recipient" placeholder="+57XXXXXXXXX">
<small class="form-text text-warning">
⚠️ Solo se pueden enviar plantillas aprobadas a números nuevos
</small>
</div>
<div class="mb-3">
<label class="form-label">Tipo de Mensaje</label>
<select class="form-control" id="message-type" onchange="toggleMessageType()">
<option value="text">Texto</option>
<option value="template">Plantilla</option>
</select>
</div>
<div class="mb-3" id="message-text-group">
<label class="form-label">Mensaje</label>
<textarea class="form-control" id="message-text" rows="4" placeholder="Escribe tu mensaje aquí..."></textarea>
</div>
<div class="mb-3" id="template-group" style="display: none;">
<label class="form-label">Plantilla</label>
<select class="form-control" id="message-template" onchange="loadMessageTemplateDetails()">
<option value="">Seleccionar plantilla...</option>
</select>
</div>
<!-- Variables de la plantilla -->
<div id="message-template-variables" class="mb-3" style="display: none;">
<!-- Se llenarán dinámicamente -->
</div>
<!-- Preview de la plantilla -->
<div id="message-template-preview" class="mb-3" style="display: none;">
<label class="form-label"><i class="fas fa-eye"></i> Vista Previa</label>
<div class="card bg-light">
<div class="card-body">
<div id="message-preview-content"></div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> Enviar Mensaje
</button>
<input type="hidden" name="template_params" id="template-params" value="">
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Broadcast Tab -->
<div id="broadcast" class="tab-content">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-lg">
<div class="card-header bg-warning text-white">
<h4 class="mb-0"><i class="fas fa-broadcast-tower"></i> Envío Masivo de Mensajes</h4>
</div>
<div class="card-body">
<div class="alert alert-success mb-4">
<i class="fas fa-check-circle"></i>
<strong>¡Envío Masivo Activo!</strong> Ahora puedes enviar mensajes a múltiples usuarios simultáneamente.
</div>
<form id="broadcast-form" onsubmit="return handleBroadcastSubmit(event)">
<div class="mb-4">
<label class="form-label fw-bold">
<i class="fas fa-filter"></i> Tipo de Selección
</label>
<select class="form-control form-control-lg" id="broadcast-selection-type" onchange="toggleBroadcastSelectionType()">
<option value="filter">📋 Por filtro de usuarios</option>
<option value="specific">👥 Usuarios específicos</option>
</select>
</div>
<!-- Filtro de usuarios -->
<div class="mb-4" id="broadcast-filter-container">
<label class="form-label fw-bold">
<i class="fas fa-filter"></i> Filtro de Usuarios
</label>
<select class="form-control form-control-lg" id="broadcast-filter">
<option value="all">📋 Todos los usuarios activos</option>
<option value="active">🔥 Solo usuarios con conversaciones recientes (7 días)</option>
<option value="recent">✨ Usuarios recientes (30 días)</option>
</select>
<small class="form-text text-muted">
<i class="fas fa-info-circle"></i> Selecciona qué usuarios recibirán el mensaje
</small>
</div>
<!-- Selección específica de usuarios -->
<div class="mb-4" id="broadcast-users-container" style="display: none;">
<label class="form-label fw-bold">
<i class="fas fa-users"></i> Seleccionar Usuarios
</label>
<select class="form-control" id="broadcast-users" multiple="multiple">
<!-- Opciones cargadas dinámicamente -->
</select>
<small class="form-text text-muted">
<i class="fas fa-info-circle"></i> Selecciona uno o más usuarios para enviar el mensaje
</small>
</div>
<div class="mb-4">
<label class="form-label fw-bold">
<i class="fas fa-envelope"></i> Tipo de Mensaje
</label>
<select class="form-control form-control-lg" id="broadcast-message-type" onchange="toggleBroadcastMessageType()">
<option value="text">📝 Mensaje de texto</option>
<option value="template">📋 Plantilla de WhatsApp</option>
</select>
</div>
<!-- Mensaje de texto -->
<div class="mb-4" id="broadcast-text-container">
<label class="form-label fw-bold">
<i class="fas fa-comment-dots"></i> Mensaje
</label>
<textarea
class="form-control"
id="broadcast-message"
rows="6"
placeholder="Escribe el mensaje que deseas enviar a todos los usuarios...
Ejemplo:
¡Hola! 👋 Tenemos una promoción especial para ti..."
maxlength="4096"></textarea>
<div class="d-flex justify-content-between mt-2">
<small class="form-text text-muted">
<i class="fas fa-keyboard"></i> Máximo 4096 caracteres
</small>
<small class="form-text text-muted">
<span id="char-count">0</span> / 4096
</small>
</div>
</div>
<!-- Selección de plantilla -->
<div class="mb-4" id="broadcast-template-container" style="display: none;">
<label class="form-label fw-bold">
<i class="fas fa-file-text"></i> Seleccionar Plantilla
</label>
<select class="form-control form-control-lg" id="broadcast-template" onchange="loadBroadcastTemplateDetails()">
<option value="">-- Selecciona una plantilla --</option>
</select>
<small class="form-text text-muted">
<i class="fas fa-info-circle"></i> Solo plantillas aprobadas
</small>
</div>
<!-- Variables de plantilla -->
<div id="broadcast-template-variables" class="mb-4" style="display: none;">
<!-- Variables dinámicas -->
</div>
<!-- Vista previa de plantilla -->
<div id="broadcast-template-preview" class="mb-4" style="display: none;">
<label class="form-label fw-bold">
<i class="fas fa-eye"></i> Vista Previa
</label>
<div class="alert alert-info" id="broadcast-preview-content">
<!-- Contenido de vista previa -->
</div>
</div>
<div class="alert alert-warning d-flex align-items-center">
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
<div>
<strong>Importante:</strong> Este mensaje se enviará a múltiples usuarios.
<br>Verifica cuidadosamente el contenido antes de enviar.
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-warning btn-lg">
<i class="fas fa-broadcast-tower"></i> Enviar Mensaje Masivo
</button>
</div>
</form>
<div class="mt-4">
<div class="card bg-light">
<div class="card-body">
<h6 class="fw-bold"><i class="fas fa-lightbulb text-warning"></i> Consejos para envío masivo:</h6>
<ul class="mb-0 small">
<li>Personaliza tu mensaje para que sea relevante</li>
<li>Evita enviar mensajes muy frecuentes</li>
<li>Respeta la ventana de 24 horas de WhatsApp</li>
<li>Usa plantillas aprobadas para nuevos contactos</li>
<li>Prueba con un grupo pequeño primero</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Templates Tab -->
<div id="templates" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-file-text"></i> Plantillas de Mensaje</h5>
<div>
<button class="btn btn-success" id="btn-sync-templates" onclick="syncTemplatesFromFacebook()">
<i class="fas fa-sync-alt"></i> Sincronizar desde Facebook
</button>
</div>
</div>
<div class="card-body">
<!-- Mensaje informativo sobre creación de plantillas -->
<div class="alert alert-info" role="alert">
<h6 class="alert-heading"><i class="fas fa-info-circle"></i> ¿Cómo crear nuevas plantillas?</h6>
<p class="mb-2">Las plantillas de WhatsApp deben crearse directamente en el Administrador de Meta Business.</p>
<p class="mb-3">Una vez creadas y aprobadas por Meta, puedes sincronizarlas aquí usando el botón "Sincronizar desde Facebook".</p>
<a href="https://business.facebook.com/latest/whatsapp_manager/message_templates?business_id=2144688622601603"
class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer">
<i class="fas fa-external-link-alt"></i> Ir al Administrador de Plantillas de Meta
</a>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Nombre</th>
<th>Plantilla WhatsApp</th>
<th>Idioma</th>
<th>Categoría</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="templates-table">
<!-- Contenido dinámico -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Auto Responses Tab -->
<div id="autoresponses" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-robot"></i> Respuestas Automáticas</h5>
<button class="btn btn-primary" onclick="showCreateAutoResponseModal()">
<i class="fas fa-plus"></i> Nueva Respuesta
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Tipo</th>
<th>Palabra Clave</th>
<th>Respuesta</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="autoresponses-table">
<!-- Contenido dinámico -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- system_config Tab -->
<div id="system_config" class="tab-content">
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-cog"></i> Configuración del Sistema</h5>
</div>
<div class="card-body">
<form id="system_config-form">
<div class="mb-4">
<h6 class="text-muted">WhatsApp Business API</h6>
<div class="mb-3">
<label class="form-label">Token de Acceso</label>
<input type="password" class="form-control" id="whatsapp-token" placeholder="Token de WhatsApp Business API">
</div>
<div class="mb-3">
<label class="form-label">Phone Number ID</label>
<input type="text" class="form-control" id="phone-number-id" placeholder="ID del número de teléfono">
</div>
<div class="mb-3">
<label class="form-label">Business Account ID (WABA ID)</label>
<input type="text" class="form-control" id="business-account-id" placeholder="ID de la cuenta de WhatsApp Business">
<small class="form-text text-muted">Necesario para sincronizar plantillas desde Facebook</small>
</div>
<div class="mb-3">
<label class="form-label">Webhook Verify Token</label>
<input type="text" class="form-control" id="webhook-token" placeholder="Token de verificación del webhook">
</div>
<div class="mb-3">
<label class="form-label">API URL</label>
<input type="text" class="form-control" id="api-url" placeholder="URL de la API" value="https://graph.facebook.com/v22.0/">
</div>
</div>
<div class="mb-4">
<h6 class="text-muted">Configuración General</h6>
<div class="mb-3">
<label class="form-label">Nombre de la Empresa</label>
<input type="text" class="form-control" id="business-name" placeholder="Nombre de tu empresa">
</div>
<div class="mb-3">
<label class="form-label">Mensaje de Bienvenida</label>
<textarea class="form-control" id="welcome-message" rows="3" placeholder="Mensaje que se envía a usuarios nuevos"></textarea>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Guardar Configuración
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Panel de Diagnóstico -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-stethoscope"></i> Diagnóstico</h5>
<button class="btn btn-sm btn-outline-primary" onclick="runDiagnostic()">
<i class="fas fa-play"></i> Ejecutar
</button>
</div>
<div class="card-body">
<div id="diagnostic-results">
<p class="text-muted">Haz clic en "Ejecutar" para verificar el estado del sistema</p>
</div>
<div class="mt-3" id="diagnostic-actions" style="display: none;">
<a href="approve_templates.html" class="btn btn-success btn-sm" target="_blank">
🔧 Solucionar Problema de Plantillas
</a>
<a href="GUIA_ERROR_PLANTILLAS.md" class="btn btn-info btn-sm" target="_blank">
📋 Ver Guía Completa
</a>
</div>
</div>
</div>
<!-- Información del Sistema -->
<div class="card">
<div class="card-header">
<h5><i class="fas fa-info-circle"></i> Información del Sistema</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Versión:</dt>
<dd class="col-sm-6">1.0.0</dd>
<dt class="col-sm-6">PHP:</dt>
<dd class="col-sm-6"><?php echo PHP_VERSION; ?></dd>
<dt class="col-sm-6">Base de Datos:</dt>
<dd class="col-sm-6"><?php echo $dbStatus; ?></dd>
<dt class="col-sm-6">Webhook URL:</dt>
<dd class="col-sm-6">
<code><?php echo APP_URL; ?>/api/webhook.php</code>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Logs Tab -->
<div id="logs" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-file-alt"></i> Logs del Sistema</h5>
<div>
<select class="form-select form-select-sm d-inline-block w-auto me-2" id="log-level-filter" onchange="filterLogs()">
<option value="all">Todos los niveles</option>
<option value="ERROR">❌ Error</option>
<option value="WARNING">⚠️ Warning</option>
<option value="INFO">️ Info</option>
<option value="DEBUG">🔧 Debug</option>
<option value="SUCCESS">✅ Success</option>
</select>
<button class="btn btn-outline-primary btn-sm" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> Actualizar
</button>
<button class="btn btn-outline-danger btn-sm" onclick="clearLogs()" title="Limpiar logs del sistema">
<i class="fas fa-trash"></i> Limpiar
</button>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<input type="text" class="form-control" id="log-search" placeholder="🔍 Buscar en logs..." onkeyup="filterLogs()">
</div>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th style="width: 140px;">Fecha/Hora</th>
<th style="width: 80px;">Nivel</th>
<th>Mensaje</th>
<th style="width: 120px;">Origen</th>
<th style="width: 80px;">Acciones</th>
</tr>
</thead>
<tbody id="logs-table">
<!-- Contenido dinámico -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
<!-- Modal - Crear Plantilla -->
<div class="modal fade" id="createTemplateModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📝 Nueva Plantilla de WhatsApp</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="template-form">
<div class="mb-3">
<label class="form-label">Nombre descriptivo *</label>
<input type="text" class="form-control" id="template-name" required placeholder="ej: Bienvenida Cliente">
<small class="form-text text-muted">Nombre interno para identificar la plantilla</small>
</div>
<div class="mb-3">
<label class="form-label">Nombre en WhatsApp *</label>
<input type="text" class="form-control" id="template-whatsapp-name" required placeholder="ej: welcome_customer">
<small class="form-text text-muted">Debe coincidir exactamente con el nombre en WhatsApp Business Manager</small>
</div>
<div class="mb-3">
<label class="form-label">Idioma</label>
<input type="text" class="form-control" id="template-language" name="template_language" value="<?= defined('APP_LANG') ? constant('APP_LANG') : 'es_ES' ?>" placeholder="ej: es_ES, en_US, pt_BR">
<small class="form-text text-muted">Código de idioma según WhatsApp (ej: es_ES, en_US, pt_BR)</small>
</div>
<div class="mb-3">
<label class="form-label">Categoría</label>
<select class="form-control" id="template-category">
<option value="utility">Utilidad</option>
<option value="marketing">Marketing</option>
<option value="authentication">Autenticación</option>
</select>
</div>
<div class="alert alert-info">
<strong>⚠️ Importante:</strong> La plantilla debe ser creada y aprobada primero en WhatsApp Business Manager.
Este formulario solo registra plantillas ya existentes en WhatsApp.
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" onclick="app.saveTemplate()">Guardar Plantilla</button>
</div>
</div>
</div>
</div>
<!-- Modal de Crear Menú -->
<div class="modal fade" id="createMenuModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📋 Nuevo Menú</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="menu-form">
<div class="mb-3">
<label class="form-label">Nombre del Menú *</label>
<input type="text" class="form-control" id="menu-name" required placeholder="ej: Menú Principal">
</div>
<div class="mb-3">
<label class="form-label">Descripción</label>
<textarea class="form-control" id="menu-description" rows="3" placeholder="Descripción del menú (opcional)"></textarea>
</div>
<div class="alert alert-info">
<strong>️ Información:</strong> Después de crear el menú, podrás agregar opciones y configurar las respuestas.
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" onclick="saveNewMenu()">Crear Menú</button>
</div>
</div>
</div>
</div>
<!-- Modal para crear/editar usuario administrador -->
<div class="modal fade" id="adminUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="adminUserModalLabel">
<i class="fas fa-user-shield"></i> <span id="admin-user-modal-title">Nuevo Usuario</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="admin-user-form">
<input type="hidden" id="admin-user-id">
<div class="mb-3">
<label class="form-label">Nombre de Usuario *</label>
<input type="text" class="form-control" id="admin-username" required
placeholder="usuario123">
<small class="text-muted">Usado para iniciar sesión</small>
</div>
<div class="mb-3">
<label class="form-label">Nombre Completo</label>
<input type="text" class="form-control" id="admin-fullname"
placeholder="Juan Pérez">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="admin-email"
placeholder="usuario@ejemplo.com">
</div>
<div class="mb-3" id="password-group">
<label class="form-label">Contraseña *</label>
<input type="password" class="form-control" id="admin-password"
placeholder="Mínimo 6 caracteres">
<small class="text-muted">Mínimo 6 caracteres</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="saveAdminUser()">
<i class="fas fa-save"></i> Guardar
</button>
</div>
</div>
</div>
</div>
<!-- Modal para cambiar contraseña -->
<div class="modal fade" id="changePasswordModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-key"></i> Cambiar Contraseña
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="change-password-user-id">
<p class="mb-3">Usuario: <strong id="change-password-username"></strong></p>
<div class="mb-3">
<label class="form-label">Nueva Contraseña *</label>
<input type="password" class="form-control" id="new-password"
placeholder="Mínimo 6 caracteres" required>
<small class="text-muted">Mínimo 6 caracteres</small>
</div>
<div class="mb-3">
<label class="form-label">Confirmar Contraseña *</label>
<input type="password" class="form-control" id="confirm-password"
placeholder="Repite la contraseña" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="saveNewPassword()">
<i class="fas fa-key"></i> Cambiar Contraseña
</button>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="./assets/js/app_simple.js?v=20"></script>
<!-- Rate Limiting Monitor Script -->
<script>
// Función para actualizar estadísticas de Rate Limiting
async function refreshRateLimitStats() {
try {
const response = await fetch('api/get_rate_limit_stats.php?action=current');
// Verificar que la respuesta sea válida
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Respuesta no-JSON recibida:', text.substring(0, 200));
throw new Error('El servidor no devolvió JSON válido');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Error obteniendo estadísticas');
}
const stats = result.data;
updateRateLimitUI(stats);
} catch (error) {
console.error('Error cargando rate limit stats:', error);
const statusEl = document.getElementById('rate-limit-status');
if (statusEl) {
statusEl.innerHTML =
'<i class="fas fa-exclamation-triangle"></i> Error cargando estadísticas: ' + error.message;
statusEl.className = 'alert alert-warning';
}
}
}
function updateRateLimitUI(stats) {
// Verificar que los elementos existan
const statusEl = document.getElementById('rate-limit-status');
const callCountBar = document.getElementById('fb-call-count-bar');
if (!statusEl || !callCountBar) {
console.log('Elementos del rate limit monitor no encontrados en el DOM');
return;
}
// Manejar caso donde Redis no está disponible
if (stats.status === 'unavailable' || stats.error) {
statusEl.className = 'alert alert-info';
statusEl.innerHTML = '<i class="fas fa-info-circle"></i> Rate Limiting Monitor: Redis no disponible. Las estadísticas de Facebook se mostrarán cuando se realice la próxima llamada a la API.';
// Mostrar mensaje en las barras de progreso
const metricsContainer = callCountBar.closest('.row');
if (metricsContainer) {
metricsContainer.innerHTML = '<div class="col-12"><div class="alert alert-info text-center"><i class="fas fa-info-circle"></i> Los datos de Facebook API se capturarán automáticamente en la próxima llamada. Redis: ' + (stats.error || 'No disponible') + '</div></div>';
}
return;
}
// Actualizar status general
const statusEl = document.getElementById('rate-limit-status');
let statusClass = 'alert-success';
let statusIcon = 'fa-check-circle';
let statusText = 'Sistema operando normalmente';
if (stats.status === 'critical') {
statusClass = 'alert-danger';
statusIcon = 'fa-exclamation-triangle';
statusText = '¡ALERTA! Rate limit cerca del límite crítico';
} else if (stats.status === 'warning') {
statusClass = 'alert-warning';
statusIcon = 'fa-exclamation-circle';
statusText = 'Advertencia: Acercándose al límite';
}
statusEl.className = `alert ${statusClass}`;
statusEl.innerHTML = `<i class="fas ${statusIcon}"></i> ${statusText}`;
// Actualizar métricas de Facebook
if (stats.app_usage) {
updateProgressBar('fb-call-count-bar', stats.app_usage.call_count || 0);
updateProgressBar('fb-total-time-bar', stats.app_usage.total_time || 0);
updateProgressBar('fb-cpu-time-bar', stats.app_usage.total_cputime || 0);
} else {
// Sin datos de Facebook aún
const callCountBar = document.getElementById('fb-call-count-bar');
if (callCountBar) {
const metricsRow = callCountBar.closest('.row');
if (metricsRow) {
metricsRow.innerHTML =
'<div class="col-12"><div class="alert alert-info text-center"><i class="fas fa-info-circle"></i> Los datos de Facebook API se capturarán automáticamente en la próxima llamada.</div></div>';
}
}
}
// Actualizar límites locales
if (stats.local_limits) {
const limitSecondEl = document.getElementById('local-limit-second-max');
const limitHourEl = document.getElementById('local-limit-hour-max');
const limitDayEl = document.getElementById('local-limit-day-max');
if (limitSecondEl) limitSecondEl.textContent = stats.local_limits.per_second;
if (limitHourEl) limitHourEl.textContent = stats.local_limits.per_hour;
if (limitDayEl) limitDayEl.textContent = stats.local_limits.per_day;
}
// Mostrar alertas si existen
const alertsSection = document.getElementById('rate-limit-alerts-section');
const alertsContainer = document.getElementById('rate-limit-alerts');
if (stats.alerts && stats.alerts.length > 0 && alertsSection && alertsContainer) {
alertsSection.style.display = 'block';
alertsContainer.innerHTML = '';
stats.alerts.slice(0, 5).forEach(alert => {
const alertClass = alert.severity === 'critical' ? 'list-group-item-danger' : 'list-group-item-warning';
const alertIcon = alert.severity === 'critical' ? 'fa-exclamation-triangle' : 'fa-exclamation-circle';
const alertEl = document.createElement('div');
alertEl.className = `list-group-item ${alertClass}`;
alertEl.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1"><i class="fas ${alertIcon}"></i> Alerta de ${alert.level}</h6>
<small>${alert.datetime}</small>
</div>
<p class="mb-1">Uso al ${alert.usage}% - Endpoint: ${alert.data.endpoint || 'N/A'}</p>
`;
alertsContainer.appendChild(alertEl);
});
} else if (alertsSection) {
alertsSection.style.display = 'none';
}
}
function updateProgressBar(id, percentage) {
const bar = document.getElementById(id);
if (!bar) return;
percentage = Math.min(100, Math.max(0, percentage));
bar.style.width = percentage + '%';
bar.textContent = percentage.toFixed(1) + '%';
// Cambiar color según el porcentaje
bar.className = 'progress-bar';
if (percentage >= 90) {
bar.classList.add('bg-danger');
} else if (percentage >= 75) {
bar.classList.add('bg-warning');
} else if (percentage >= 50) {
bar.classList.add('bg-info');
} else {
bar.classList.add('bg-success');
}
}
// Cargar estadísticas al cargar la página
document.addEventListener('DOMContentLoaded', function() {
// Cargar inmediatamente
refreshRateLimitStats();
// Actualizar cada 30 segundos
setInterval(refreshRateLimitStats, 30000);
});
// Actualizar también cuando se cambia a la pestaña de dashboard
document.querySelectorAll('.nav-link[data-tab="dashboard"]').forEach(link => {
link.addEventListener('click', function() {
setTimeout(refreshRateLimitStats, 500);
});
});
</script>
<script>
// Función de diagnóstico mejorada
function runDiagnostic() {
const resultsDiv = document.getElementById('diagnostic-results');
const actionsDiv = document.getElementById('diagnostic-actions');
resultsDiv.innerHTML = '<p class="text-muted"><i class="fas fa-spinner fa-spin"></i> Ejecutando diagnóstico...</p>';
fetch('/api/diagnose_whatsapp.php')
.then(response => response.json())
.then(data => {
if (data.success) {
let html = '<div class="diagnostic-report">';
// Configuración
html += `<div class="mb-3">
<h6><i class="fas fa-cog"></i> Configuración WhatsApp</h6>
<div class="alert alert-${data.config.status === 'complete' ? 'success' : 'warning'}">
${data.config.message} (${data.config.completion}% completa)
</div>
</div>`;
// Plantillas
const templateIssues = data.templates.approved === 0 && data.templates.pending > 0;
html += `<div class="mb-3">
<h6><i class="fas fa-file-alt"></i> Estado de Plantillas</h6>
<div class="alert alert-${templateIssues ? 'danger' : 'success'}">
${data.templates.message}
<br><small>Aprobadas: ${data.templates.approved} | Pendientes: ${data.templates.pending}</small>
</div>
</div>`;
// API
let apiMessage = '';
if (typeof data.api.message === 'object' && data.api.message !== null) {
// Si el mensaje es un objeto de error
if (data.api.message.message) {
apiMessage = `<strong>Error:</strong> ${data.api.message.message.split(':')[0]}`;
apiMessage += `<br><small class="text-muted">Verifica tu token de acceso y Phone Number ID</small>`;
} else {
apiMessage = 'Error en la conectividad de la API';
}
} else {
// Si el mensaje es un string simple
apiMessage = data.api.message;
}
html += `<div class="mb-3">
<h6><i class="fas fa-plug"></i> Conectividad API</h6>
<div class="alert alert-${data.api.status === 'ready' ? 'success' : 'danger'}">
${apiMessage}
</div>
</div>`;
// Recomendaciones
if (data.recommendations && data.recommendations.length > 0) {
html += `<div class="mb-3">
<h6><i class="fas fa-lightbulb"></i> Recomendaciones</h6>
<ul class="list-unstyled">`;
data.recommendations.forEach(rec => {
html += `<li class="mb-1"><i class="fas fa-arrow-right text-primary"></i> ${rec}</li>`;
});
html += `</ul></div>`;
}
html += '</div>';
resultsDiv.innerHTML = html;
// Mostrar botones de acción si hay problemas con plantillas
if (templateIssues) {
actionsDiv.style.display = 'block';
} else {
actionsDiv.style.display = 'none';
}
} else {
resultsDiv.innerHTML = `<div class="alert alert-danger">Error: ${data.message}</div>`;
actionsDiv.style.display = 'none';
}
})
.catch(error => {
console.error('Error:', error);
resultsDiv.innerHTML = '<div class="alert alert-danger">Error de conectividad</div>';
actionsDiv.style.display = 'none';
});
}
// Lightbox helper para imágenes (abre un modal Bootstrap con la imagen en grande)
function openImageLightbox(url, caption = '') {
let img = document.getElementById('imageLightboxImg');
let cap = document.getElementById('imageLightboxCaption');
if (!img || !cap) return; // seguridad
img.src = url;
cap.textContent = caption || '';
const modalEl = document.getElementById('imageLightboxModal');
const bsModal = new bootstrap.Modal(modalEl);
bsModal.show();
}
</script>
<script>
// Enhancer: pedir nombre para la plantilla `contacto_nuevo` antes de enviar
(function(){
const form = document.getElementById('send-message-form');
if (!form) return;
form.addEventListener('submit', function(e){
try {
const typeEl = document.getElementById('message-type');
const tplEl = document.getElementById('message-template');
const recipientTypeEl = document.getElementById('recipient-type');
if (!typeEl || !tplEl) return; // nothing to do
const type = typeEl.value;
const tpl = tplEl.value;
if (type === 'template' && tpl === 'contacto_nuevo') {
// obtener valor por defecto para el prompt
let defaultName = '';
if (recipientTypeEl && recipientTypeEl.value === 'existing') {
const sel = document.getElementById('message-recipient');
if (sel && sel.options && sel.selectedIndex >= 0) defaultName = sel.options[sel.selectedIndex].text || '';
} else {
const manual = document.getElementById('manual-recipient');
if (manual) defaultName = manual.value || '';
}
const name = prompt('Ingrese el nombre para la plantilla contacto_nuevo:', defaultName);
if (name === null) {
// usuario canceló
e.preventDefault();
return false;
}
const paramsInput = document.getElementById('template-params');
if (paramsInput) {
const clean = (typeof name === 'string') ? name.trim() : '';
paramsInput.value = JSON.stringify({ name: clean || defaultName });
}
}
} catch (err) {
console.error('template name prompt failed', err);
}
});
})();
</script>
<!-- Lightbox Modal -->
<div class="modal fade" id="imageLightboxModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-transparent border-0">
<div class="modal-body p-0 position-relative">
<button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-3" data-bs-dismiss="modal" aria-label="Cerrar" style="z-index:10;"></button>
<img id="imageLightboxImg" src="" class="img-fluid w-100" style="border-radius:10px;">
<div id="imageLightboxCaption" class="mt-2 text-center text-white small"></div>
</div>
</div>
</div>
</div>
<!-- Modal para crear menú -->
<div class="modal fade" id="createMenuModal" tabindex="-1" aria-labelledby="createMenuModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createMenuModalLabel">
<i class="fas fa-plus"></i> Crear Nuevo Menú
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="menu-form">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Nombre del Menú *</label>
<input type="text" class="form-control" id="menu-name" placeholder="ej: Menú Principal" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Clave del Menú *</label>
<input type="text" class="form-control" id="menu-key" placeholder="ej: main_menu" required>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Descripción</label>
<textarea class="form-control" id="menu-description" rows="2" placeholder="Descripción del menú"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Mensaje de Bienvenida</label>
<textarea class="form-control" id="menu-welcome-message" rows="3" placeholder="¡Hola! Bienvenido. Por favor selecciona una opción:"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Estado</label>
<select class="form-control" id="menu-status">
<option value="active">Activo</option>
<option value="inactive">Inactivo</option>
</select>
</div>
<hr>
<h6>📋 Opciones del Menú</h6>
<div id="menu-options-container">
<!-- Opciones se agregan dinámicamente -->
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="addMenuOption()">
<i class="fas fa-plus"></i> Agregar Opción
</button>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="app.saveMenu()">
<i class="fas fa-save"></i> Guardar Menú
</button>
</div>
</div>
</div>
</div>
<!-- Modal para crear/editar autorespuesta -->
<div class="modal fade" id="createAutoResponseModal" tabindex="-1" aria-labelledby="createAutoResponseModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createAutoResponseModalLabel">
<i class="fas fa-robot"></i> Crear Nueva Respuesta Automática
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="autoresponse-form">
<input type="hidden" id="autoresponse-id">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Tipo de Trigger *</label>
<select class="form-control" id="autoresponse-trigger-type" required onchange="updateTriggerFields(this.value)">
<option value="keyword">Palabra Clave</option>
<option value="contains">Contiene Texto</option>
<option value="exact">Mensaje Exacto</option>
<option value="welcome">Mensaje de Bienvenida</option>
<option value="default">Respuesta por Defecto</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Prioridad</label>
<input type="number" class="form-control" id="autoresponse-priority" value="1" min="0" max="10">
<small class="text-muted">Mayor número = mayor prioridad (0-10)</small>
</div>
</div>
</div>
<div class="mb-3" id="trigger-value-group">
<label class="form-label" id="trigger-value-label">Palabras Clave *</label>
<input type="text" class="form-control" id="autoresponse-trigger-value" placeholder="palabra1,palabra2,palabra3">
<small class="text-muted" id="trigger-value-help">Separa múltiples palabras con comas</small>
</div>
<div class="mb-3">
<label class="form-label">Texto de Respuesta *</label>
<textarea class="form-control" id="autoresponse-text" rows="4" placeholder="Escribe aquí la respuesta que se enviará automáticamente..." required></textarea>
<small class="text-muted">Puedes usar emojis y saltos de línea</small>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Tipo de Respuesta</label>
<select class="form-control" id="autoresponse-type">
<option value="text">Mensaje de Texto</option>
<option value="template">Plantilla WhatsApp</option>
<option value="menu">Mostrar Menú</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Estado</label>
<select class="form-control" id="autoresponse-active">
<option value="1">Activa</option>
<option value="0">Inactiva</option>
</select>
</div>
</div>
</div>
<!-- Campos adicionales para diferentes tipos de respuesta -->
<div id="template-fields" style="display: none;">
<div class="mb-3">
<label class="form-label">Nombre de la Plantilla</label>
<input type="text" class="form-control" id="autoresponse-template-name" placeholder="nombre_plantilla">
</div>
</div>
<div id="menu-fields" style="display: none;">
<div class="mb-3">
<label class="form-label">Menú a Mostrar</label>
<select class="form-control" id="autoresponse-menu-id">
<option value="">Seleccionar menú...</option>
<!-- Opciones cargadas dinámicamente -->
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancelar
</button>
<button type="button" class="btn btn-primary" onclick="app.saveAutoResponse()">
<i class="fas fa-save"></i> Guardar Respuesta
</button>
</div>
</div>
</div>
</div>
<script>
// Global Notification Manager: polls server for unread notifications and shows toasts + native notifications
const NotificationManager = {
interval: 7000,
// IDs de notificaciones ya mostradas para evitar re-sonar/repetir
_seenIds: new Set(),
init() {
this.bell = document.getElementById('notification-bell');
this.countEl = document.getElementById('notification-count');
this.toastsContainer = document.getElementById('global-notification-toasts');
if (this.bell) {
this.bell.addEventListener('click', () => {
// switch to conversations tab
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
// Request notification permission on explicit interaction
if ("Notification" in window && Notification.permission === 'default') {
Notification.requestPermission().then(p => console.log('Notification permission:', p));
}
// Al abrir la campana se considera que se atendieron visualmente las notificaciones: limpiar indicador visual
this.bell.classList.remove('has-notifications');
});
}
this.loadNotifications();
this._timer = setInterval(() => this.loadNotifications(), this.interval);
},
async loadNotifications() {
try {
const resp = await fetch('api/get_notifications.php', { credentials: 'same-origin' });
if (resp.status === 401) {
console.warn('Notifications fetch: unauthorized (session may have expired)');
this.showSessionExpiredToast();
return;
}
const json = await resp.json();
console.debug('NotificationManager.loadNotifications response:', json);
if (json && json.success && Array.isArray(json.data)) {
const unreadCount = json.data.length;
if (unreadCount > 0) {
this.countEl.textContent = unreadCount;
this.countEl.style.display = 'inline-block';
this.bell.classList.add('has-notifications');
} else {
this.countEl.style.display = 'none';
this.bell.classList.remove('has-notifications');
}
// Mostrar sólo las nuevas notificaciones (no repetir sonido/visual)
json.data.forEach(n => {
if (!this._seenIds.has(n.id)) {
this._seenIds.add(n.id);
this.showToast(n);
}
});
} else if (json && json.success === false) {
console.warn('Notification API error:', json.error || json);
}
} catch (e) {
console.error('Error loading global notifications', e);
}
},
showSessionExpiredToast() {
const toast = document.createElement('div');
toast.className = 'notification-toast';
toast.style.background = '#fff7f7';
toast.style.border = '1px solid #f5c6cb';
toast.style.padding = '10px 14px';
toast.style.marginTop = '8px';
toast.style.borderRadius = '6px';
toast.style.minWidth = '220px';
const msg = document.createElement('div');
msg.textContent = 'La sesión ha expirado. Por favor, vuelve a iniciar sesión.';
toast.appendChild(msg);
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary';
btn.textContent = 'Ir a login';
btn.onclick = () => { window.location.href = 'login.php'; };
const actions = document.createElement('div');
actions.style.marginTop = '8px';
actions.appendChild(btn);
toast.appendChild(actions);
this.toastsContainer.appendChild(toast);
// auto-eliminar toast de sesión expirada después de 1s
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 1000);
},
showToast(notification) {
const toast = document.createElement('div');
toast.className = 'notification-toast';
toast.style.background = '#fff';
toast.style.padding = '10px 14px';
toast.style.border = '1px solid #ddd';
toast.style.boxShadow = '0 2px 6px rgba(0,0,0,0.12)';
toast.style.marginTop = '8px';
toast.style.borderRadius = '6px';
toast.style.minWidth = '220px';
const msg = document.createElement('div');
msg.textContent = notification.message || 'Nuevo evento';
toast.appendChild(msg);
const actions = document.createElement('div');
actions.style.marginTop = '8px';
actions.style.display = 'flex';
actions.style.gap = '8px';
const openBtn = document.createElement('button');
openBtn.className = 'btn btn-sm btn-primary';
openBtn.textContent = 'Abrir';
openBtn.onclick = async () => {
await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})});
let data = {};
try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {}
const userId = data.user_id || notification.user_id;
if (userId) {
// marcar conversaciones como leídas también
try { await fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})}); } catch(e) { console.warn('mark_conversation_read failed', e); }
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
const tryOpen = () => {
if (window.chatApp && typeof window.chatApp.openConversation === 'function') {
window.chatApp.openConversation(userId, 'Usuario', '');
} else {
setTimeout(tryOpen, 300);
}
};
tryOpen();
}
toast.remove();
};
const dismiss = document.createElement('button');
dismiss.className = 'btn btn-sm btn-secondary';
dismiss.textContent = 'Descartar';
dismiss.onclick = async () => {
await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})});
toast.remove();
};
actions.appendChild(openBtn);
actions.appendChild(dismiss);
toast.appendChild(actions);
this.toastsContainer.appendChild(toast);
// native notification
if ("Notification" in window && Notification.permission === 'granted') {
try {
const data = notification.data ? JSON.parse(notification.data) : {};
const n = new Notification(notification.message || 'Nuevo evento', { body: (data.phone || '') });
n.onclick = function() {
window.focus();
const userId = data.user_id || notification.user_id;
if (userId) {
// marcar conversación leída cuando se abre desde la notification nativa
try { fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})}); } catch(e) { console.warn('mark_conversation_read failed', e); }
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
const tryOpen = () => {
if (window.chatApp && typeof window.chatApp.openConversation === 'function') {
window.chatApp.openConversation(userId, 'Usuario', '');
} else {
setTimeout(tryOpen, 300);
}
};
tryOpen();
}
n.close();
};
// cerrar la notificación nativa automáticamente después de 1s
setTimeout(() => { try { n.close(); } catch(e) {} }, 1000);
} catch (e) { console.warn('native notification failed', e); }
}
// play sound
try {
// usar Web Audio API para reproducir un sonido breve
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.value = 880;
o.connect(g);
g.connect(ctx.destination);
g.gain.value = 0.05;
o.start();
setTimeout(() => { o.stop(); ctx.close(); }, 180);
} catch (e) { console.warn('Notification sound failed', e); }
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 1000);
}
};
// ====== Funciones para Broadcast con Plantillas y Múltiples Usuarios ======
// Manejador del formulario de broadcast
async function handleBroadcastSubmit(event) {
event.preventDefault();
event.stopPropagation();
console.log('Formulario de broadcast enviado');
// Validar que existe el manager
if (!window.whatsappManager || !window.whatsappManager.sendBroadcast) {
console.error('whatsappManager no disponible');
alert('Error: Sistema no inicializado. Recarga la página.');
return false;
}
// Validar variables de plantilla si se seleccionó plantilla
const messageType = document.getElementById('broadcast-message-type').value;
if (messageType === 'template') {
const templateId = document.getElementById('broadcast-template').value;
if (!templateId) {
alert('Por favor selecciona una plantilla');
return false;
}
// Validar que las variables requeridas estén llenas
const varInputs = document.querySelectorAll('.broadcast-template-var');
let hasEmpty = false;
varInputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('is-invalid');
hasEmpty = true;
} else {
input.classList.remove('is-invalid');
}
});
if (hasEmpty) {
alert('Por favor completa todos los campos de variables de la plantilla');
return false;
}
}
// Llamar al método del manager
await window.whatsappManager.sendBroadcast();
return false;
}
function initializeBroadcastSelect2() {
// Inicializar Select2 para usuarios con transport personalizado
$('#broadcast-users').select2({
theme: 'bootstrap-5',
placeholder: 'Buscar y seleccionar usuarios...',
allowClear: true,
ajax: {
transport: function (params, success, failure) {
const searchTerm = params.data.term || '';
const page = params.data.page || 1;
fetch(`api/get_users.php?search=${encodeURIComponent(searchTerm)}&page=${page}&limit=50`, {
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
success(data);
} else {
failure();
}
})
.catch(error => {
console.error('Error cargando usuarios:', error);
failure();
});
},
delay: 250,
processResults: function (data, params) {
params.page = params.page || 1;
if (!data || !data.data) {
return { results: [] };
}
return {
results: data.data.map(user => ({
id: user.id,
text: `${user.name || 'Sin nombre'} (${user.phone_number})`
})),
pagination: {
more: data.pagination && data.pagination.hasNext
}
};
},
cache: true
},
minimumInputLength: 0
});
// Cargar plantillas aprobadas
loadBroadcastTemplates();
}
function toggleBroadcastSelectionType() {
const selectionType = document.getElementById('broadcast-selection-type').value;
const filterContainer = document.getElementById('broadcast-filter-container');
const usersContainer = document.getElementById('broadcast-users-container');
if (selectionType === 'filter') {
filterContainer.style.display = 'block';
usersContainer.style.display = 'none';
} else {
filterContainer.style.display = 'none';
usersContainer.style.display = 'block';
}
}
function toggleBroadcastMessageType() {
const messageType = document.getElementById('broadcast-message-type').value;
const textContainer = document.getElementById('broadcast-text-container');
const templateContainer = document.getElementById('broadcast-template-container');
if (messageType === 'text') {
textContainer.style.display = 'block';
templateContainer.style.display = 'none';
document.getElementById('broadcast-template-variables').style.display = 'none';
document.getElementById('broadcast-template-preview').style.display = 'none';
} else {
textContainer.style.display = 'none';
templateContainer.style.display = 'block';
}
}
async function loadBroadcastTemplates() {
try {
console.log('🔵 Cargando plantillas para broadcast...');
if (!window.whatsappManager) {
console.error('❌ whatsappManager no está disponible');
return;
}
const response = await window.whatsappManager.apiCall('get_templates.php');
console.log('Respuesta de get_templates:', response);
if (response && response.success && response.data) {
const select = document.getElementById('broadcast-template');
if (!select) {
console.error('Elemento broadcast-template no encontrado');
return;
}
select.innerHTML = '<option value="">-- Selecciona una plantilla --</option>';
const approvedTemplates = response.data.filter(t =>
t.status && (t.status.toUpperCase() === 'APPROVED' || t.status.toLowerCase() === 'approved')
);
console.log('Plantillas aprobadas encontradas:', approvedTemplates.length);
approvedTemplates.forEach(template => {
const option = document.createElement('option');
option.value = template.id;
option.textContent = `${template.name} (${template.language_code || template.language || 'es'})`;
option.dataset.templateId = template.id;
select.appendChild(option);
});
if (approvedTemplates.length === 0) {
console.warn('No hay plantillas aprobadas disponibles');
select.innerHTML = '<option value="">No hay plantillas aprobadas</option>';
}
} else {
console.error('Respuesta inválida del API:', response);
}
} catch (error) {
console.error('Error cargando plantillas:', error);
}
}
async function loadBroadcastTemplateDetails() {
console.log('🔵 loadBroadcastTemplateDetails llamado');
const select = document.getElementById('broadcast-template');
const templateId = select.value;
console.log('Template ID seleccionado:', templateId);
if (!templateId) {
document.getElementById('broadcast-template-variables').style.display = 'none';
document.getElementById('broadcast-template-preview').style.display = 'none';
return;
}
try {
const response = await window.whatsappManager.apiCall(`get_template_details.php?id=${templateId}`);
console.log('✅ Detalles de plantilla:', response);
if (response && response.success) {
const template = response.template || response.data;
if (!template) {
console.error('❌ No se encontró template en la respuesta');
return;
}
console.log('📋 Template data:', template);
console.log('📋 Variables:', template.variables);
const variablesContainer = document.getElementById('broadcast-template-variables');
if (template.variables && template.variables.length > 0) {
console.log(`✅ Se encontraron ${template.variables.length} variables`);
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
template.variables.forEach((variable, index) => {
console.log(`Variable ${index}:`, variable);
const placeholder = variable.example || `Valor para Variable ${variable.index}`;
const label = variable.label || `Variable ${variable.index}`;
html += `
<div class="mb-2">
<label class="form-label small">${label}</label>
<input
type="text"
class="form-control broadcast-template-var"
data-var="${variable.index}"
data-placeholder="${variable.placeholder}"
placeholder="${placeholder}"
onkeyup="updateBroadcastPreview()"
>
</div>
`;
});
console.log('📝 HTML generado para variables:', html);
variablesContainer.innerHTML = html;
variablesContainer.style.display = 'block';
console.log('✅ Variables container mostrado');
} else {
console.log('⚠️ Template sin variables');
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
variablesContainer.style.display = 'block';
}
updateBroadcastPreview();
} else {
console.error('❌ Respuesta sin success:', response);
}
} catch (error) {
console.error('❌ Error cargando detalles de plantilla:', error);
window.whatsappManager.showError('Error cargando plantilla');
}
}
async function updateBroadcastPreview() {
const templateId = document.getElementById('broadcast-template').value;
if (!templateId) return;
// Recolectar variables como array (por índice)
const variablesArray = [];
document.querySelectorAll('.broadcast-template-var').forEach(input => {
const index = parseInt(input.dataset.var) - 1; // Convertir a índice de array (0-based)
variablesArray[index] = input.value || `{{${input.dataset.var}}}`;
});
try {
const response = await window.whatsappManager.apiCall('preview_template.php', {
method: 'POST',
body: { template_id: templateId, parameters: variablesArray }
});
if (response && response.success && response.preview) {
const previewContainer = document.getElementById('broadcast-template-preview');
const previewContent = document.getElementById('broadcast-preview-content');
// Usar el HTML pre-formateado del servidor
previewContent.innerHTML = response.preview.html || response.preview.body.replace(/\n/g, '<br>');
previewContainer.style.display = 'block';
} else {
console.error('Error en preview:', response);
}
} catch (error) {
console.error('Error generando vista previa:', error);
}
}
// ====== Fin de funciones de Broadcast ======
document.addEventListener('DOMContentLoaded', () => {
try { NotificationManager.init(); } catch(e) { console.error('NotificationManager init failed', e); }
// Inicializar Select2 para broadcast cuando el manager esté listo
const waitForManager = setInterval(() => {
if (window.whatsappManager) {
clearInterval(waitForManager);
console.log('Inicializando broadcast Select2...');
try {
initializeBroadcastSelect2();
} catch(e) {
console.error('Error inicializando broadcast Select2:', e);
}
}
}, 100);
// Timeout de seguridad
setTimeout(() => clearInterval(waitForManager), 5000);
// Sidebar toggle logic for responsive/mobile
const sidebar = document.querySelector('.sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
let overlayEl = null;
function ensureOverlay() {
if (!overlayEl) {
overlayEl = document.createElement('div');
overlayEl.className = 'sidebar-overlay';
document.body.appendChild(overlayEl);
overlayEl.addEventListener('click', closeSidebar);
}
}
function openSidebar() {
if (!sidebar) return;
sidebar.classList.add('open');
document.body.classList.add('sidebar-open');
ensureOverlay();
overlayEl.style.display = 'block';
}
function closeSidebar() {
if (!sidebar) return;
sidebar.classList.remove('open');
document.body.classList.remove('sidebar-open');
if (overlayEl) overlayEl.style.display = 'none';
}
if (toggleBtn) {
console.debug('[index] sidebar toggle found:', { toggleBtn: !!toggleBtn, sidebar: !!sidebar });
toggleBtn.addEventListener('click', (e) => {
console.debug('[index] sidebar-toggle clicked', { isOpen: sidebar && sidebar.classList.contains('open') });
e.stopPropagation();
if (sidebar && sidebar.classList.contains('open')) {
closeSidebar();
} else {
openSidebar();
}
});
}
// debug logs for open/close
const _openSidebar = openSidebar;
const _closeSidebar = closeSidebar;
openSidebar = function() { console.debug('[index] openSidebar()'); _openSidebar(); };
closeSidebar = function() { console.debug('[index] closeSidebar()'); _closeSidebar(); };
// Close sidebar when a nav item is selected (mobile UX improvement)
document.querySelectorAll('.nav-link[data-tab]').forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth < 992) {
closeSidebar();
}
});
});
// Close sidebar on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSidebar();
});
// Close sidebar when navigating in main content on small screens
document.querySelector('.main-content')?.addEventListener('click', () => {
if (window.innerWidth < 992) closeSidebar();
});
});
</script>
</body>
</html>