2196 lines
121 KiB
PHP
2196 lines
121 KiB
PHP
<?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();
|
||
$pdo = $db->getConnection();
|
||
$pdo->query('SELECT 1'); // prueba real
|
||
$dbHost = DB_HOST ?? 'desconocido';
|
||
$serverIp = $_SERVER['SERVER_ADDR'] ?? gethostbyname(gethostname());
|
||
$dbStatus = '<span class="text-success fw-bold">Conectado</span> <small class="text-muted">(' . htmlspecialchars($dbHost) . ')</small>';
|
||
} catch (Exception $e) {
|
||
$dbStatus = '<span class="text-danger fw-bold">Error:</span> ' . htmlspecialchars($e->getMessage());
|
||
$serverIp = $_SERVER['SERVER_ADDR'] ?? '—';
|
||
}
|
||
?>
|
||
<!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</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="#whatsapp_profile" class="nav-link" data-tab="whatsapp_profile"><i class="fab fa-whatsapp"></i> Perfil WhatsApp</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><a href="#terms" class="nav-link" data-tab="terms"><i class="fas fa-file-contract"></i> T&C Aceptaciones</a></li>
|
||
<li class="sidebar-divider"></li>
|
||
<li><a href="lab_dashboard.php" class="nav-link text-warning fw-semibold"><i class="fas fa-flask"></i> Módulo Laboratorio <span class="badge bg-warning text-dark ms-1" style="font-size:.65rem">LAB</span></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">
|
||
|
||
|
||
<!-- 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> <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> 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 d-flex align-items-center gap-2">
|
||
<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>
|
||
<button class="btn btn-light btn-sm text-nowrap" onclick="exportConversations()" title="Exportar todas las conversaciones a CSV">
|
||
<i class="fas fa-file-csv"></i> Exportar
|
||
</button>
|
||
</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 border-0 shadow-sm">
|
||
<div class="card-body text-center py-5">
|
||
<i class="fas fa-users-cog fa-3x text-primary mb-3"></i>
|
||
<h5 class="fw-semibold mb-2">Gestión de Usuarios & Roles</h5>
|
||
<p class="text-muted mb-4">
|
||
La administración de usuarios, roles y permisos de módulo<br>
|
||
se encuentra centralizada en el Módulo de Laboratorio.
|
||
</p>
|
||
<a href="lab_usuarios.php" class="btn btn-primary px-4">
|
||
<i class="fas fa-external-link-alt me-2"></i>Ir a Usuarios & Roles
|
||
</a>
|
||
</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>
|
||
|
||
<!-- WhatsApp Profile Tab -->
|
||
<div id="whatsapp_profile" class="tab-content">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5><i class="fab fa-whatsapp text-success"></i> Perfil de WhatsApp Business</h5>
|
||
<button class="btn btn-outline-primary btn-sm" onclick="loadWhatsAppProfile()">
|
||
<i class="fas fa-sync-alt"></i> Recargar desde WhatsApp
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<!-- Foto de perfil -->
|
||
<div class="col-md-3 text-center mb-4">
|
||
<div class="mb-3">
|
||
<img id="wp-profile-photo-preview" src="" alt="Foto de perfil"
|
||
style="width:120px;height:120px;border-radius:50%;object-fit:cover;border:3px solid #25D366;display:none;">
|
||
<div id="wp-profile-photo-placeholder"
|
||
style="width:120px;height:120px;border-radius:50%;background:#f0f0f0;border:3px solid #ccc;display:inline-flex;align-items:center;justify-content:center;">
|
||
<i class="fas fa-user fa-3x text-muted"></i>
|
||
</div>
|
||
</div>
|
||
<div class="mb-2">
|
||
<label class="form-label small fw-bold">Cambiar foto de perfil</label>
|
||
<input type="file" id="wp-photo-file" class="form-control form-control-sm" accept="image/jpeg,image/png">
|
||
<small class="text-muted">JPG o PNG, máx. 5 MB</small>
|
||
</div>
|
||
<button class="btn btn-success btn-sm w-100" onclick="uploadWhatsAppProfilePhoto()">
|
||
<i class="fas fa-upload"></i> Subir foto
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Datos del perfil -->
|
||
<div class="col-md-9">
|
||
<form id="wp-profile-form">
|
||
<div class="mb-3">
|
||
<label class="form-label">Descripción (about) <small class="text-muted">— visible en el perfil de WhatsApp</small></label>
|
||
<textarea class="form-control" id="wp-about" rows="2" maxlength="139" placeholder="Ej: Horario de atención: L-V 8am-6pm"></textarea>
|
||
<small class="text-muted">Máx. 139 caracteres</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Descripción larga</label>
|
||
<textarea class="form-control" id="wp-description" rows="3" maxlength="512" placeholder="Descripción detallada del negocio"></textarea>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Dirección</label>
|
||
<input type="text" class="form-control" id="wp-address" placeholder="Dirección física del negocio">
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Correo electrónico</label>
|
||
<input type="email" class="form-control" id="wp-email" placeholder="contacto@empresa.com">
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Sitio web</label>
|
||
<input type="url" class="form-control" id="wp-website" placeholder="https://www.empresa.com">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Categoría de negocio</label>
|
||
<select class="form-select" id="wp-vertical">
|
||
<option value="">— Seleccionar —</option>
|
||
<option value="UNDEFINED">Sin categoría</option>
|
||
<option value="OTHER">Otro</option>
|
||
<option value="AUTO">Automotriz</option>
|
||
<option value="BEAUTY">Belleza / Spa</option>
|
||
<option value="APPAREL">Ropa / Moda</option>
|
||
<option value="EDU">Educación</option>
|
||
<option value="ENTERTAIN">Entretenimiento</option>
|
||
<option value="EVENT_PLAN">Eventos</option>
|
||
<option value="FINANCE">Finanzas</option>
|
||
<option value="GROCERY">Supermercado</option>
|
||
<option value="GOVT">Gobierno</option>
|
||
<option value="HOTEL">Hotel</option>
|
||
<option value="HEALTH">Salud / Medicina</option>
|
||
<option value="NONPROFIT">Organización sin fines de lucro</option>
|
||
<option value="PROF_SERVICES">Servicios profesionales</option>
|
||
<option value="RETAIL">Comercio minorista</option>
|
||
<option value="TRAVEL">Viajes / Turismo</option>
|
||
<option value="RESTAURANT">Restaurante</option>
|
||
<option value="NOT_A_BIZ">No es un negocio</option>
|
||
</select>
|
||
</div>
|
||
<button type="button" class="btn btn-primary" onclick="saveWhatsAppProfile()">
|
||
<i class="fas fa-save"></i> Guardar cambios de perfil
|
||
</button>
|
||
</form>
|
||
</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>
|
||
<button class="btn btn-danger ms-2" id="btn-reset-sync-templates" onclick="resetAndSyncTemplates()">
|
||
<i class="fas fa-trash-restore"></i> Limpiar y Re-sincronizar
|
||
</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 class="mb-3">
|
||
<label class="form-label">
|
||
<i class="fas fa-file-contract me-1 text-primary"></i>Mensaje de Términos y Condiciones
|
||
</label>
|
||
<textarea class="form-control" id="terms-message" rows="4"
|
||
placeholder="Mensaje de aceptación de términos y condiciones. Se muestra antes de cualquier interacción."></textarea>
|
||
<small class="form-text text-muted">Este mensaje se muestra al usuario antes de aceptar cualquier cosa. Incluye las opciones de aceptar o rechazar.</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">
|
||
<i class="fas fa-ban me-1 text-danger"></i>Mensaje cuando rechaza los términos
|
||
</label>
|
||
<textarea class="form-control" id="terms-rejected-message" rows="3"
|
||
placeholder="Mensaje que se envía cuando el usuario rechaza los términos y condiciones."></textarea>
|
||
<small class="form-text text-muted">Se envía automáticamente si el usuario elige no aceptar.</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── GESTIÓN DOCUMENTO PDF TÉRMINOS ─────────────────────────── -->
|
||
<div class="card border-0 bg-light mb-4">
|
||
<div class="card-body p-3">
|
||
<h6 class="mb-3"><i class="fas fa-file-pdf text-danger me-1"></i>Documento PDF de Términos</h6>
|
||
|
||
<div class="mb-3" id="terms-doc-current" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 p-2 bg-white rounded border">
|
||
<i class="fas fa-file-pdf text-danger fa-lg"></i>
|
||
<div class="flex-grow-1 overflow-hidden">
|
||
<div class="text-truncate small fw-bold" id="terms-doc-name">—</div>
|
||
<div class="text-muted" style="font-size:.75rem;">Versión: <span id="terms-doc-version">—</span></div>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" id="terms-doc-copy-btn" title="Copiar URL">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
<a href="#" target="_blank" id="terms-doc-link" class="btn btn-sm btn-outline-primary" title="Abrir PDF">
|
||
<i class="fas fa-external-link-alt"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label small">Versión del nuevo documento <span class="text-muted">(ej. 2026-06)</span></label>
|
||
<input type="text" class="form-control form-control-sm" id="terms-new-version" placeholder="2026-06">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label small">Subir nuevo PDF</label>
|
||
<input class="form-control form-control-sm" type="file" id="terms-pdf-file" accept="application/pdf">
|
||
</div>
|
||
|
||
<div class="mb-3 form-check">
|
||
<input class="form-check-input" type="checkbox" id="terms-force-reaccept">
|
||
<label class="form-check-label small" for="terms-force-reaccept">
|
||
<strong>Forzar re-aceptación a todos los usuarios</strong>
|
||
<span class="text-muted d-block" style="font-size:.75rem;">Al guardar, todos los usuarios deberán aceptar los nuevos términos la próxima vez que escriban.</span>
|
||
</label>
|
||
</div>
|
||
|
||
<button type="button" class="btn btn-danger btn-sm" id="terms-upload-btn">
|
||
<i class="fas fa-upload me-1"></i>Guardar documento y configuración
|
||
</button>
|
||
<div id="terms-upload-status" class="mt-2 small" style="display:none;"></div>
|
||
</div>
|
||
</div>
|
||
<!-- ── / GESTIÓN DOCUMENTO PDF ────────────────────────────────── -->
|
||
|
||
<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"><span class="badge bg-primary">v<?php echo APP_VERSION; ?></span></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">IP del Servidor:</dt>
|
||
<dd class="col-sm-6"><code><?php echo htmlspecialchars($serverIp); ?></code></dd>
|
||
|
||
<dt class="col-sm-6">Webhook URL:</dt>
|
||
<dd class="col-sm-6">
|
||
<code><?php echo htmlspecialchars(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>
|
||
<!-- T&C Aceptaciones Tab -->
|
||
<div id="terms" class="tab-content">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||
<h5 class="mb-0"><i class="fas fa-file-contract me-2"></i>Aceptaciones de Términos y Condiciones</h5>
|
||
<div class="d-flex gap-2 align-items-center flex-wrap">
|
||
<!-- Filtros -->
|
||
<select id="terms-filter-estado" class="form-select form-select-sm" style="width:auto;">
|
||
<option value="">Todos los estados</option>
|
||
<option value="pendiente">Pendiente</option>
|
||
<option value="aceptado">Aceptado</option>
|
||
<option value="rechazado">Rechazado</option>
|
||
</select>
|
||
<input type="date" id="terms-filter-fecha" class="form-control form-control-sm" style="width:auto;">
|
||
<input type="text" id="terms-filter-phone" class="form-control form-control-sm" placeholder="Teléfono..." style="width:140px;">
|
||
<button class="btn btn-sm btn-primary" onclick="termsAdmin.cargar()"><i class="fas fa-search"></i></button>
|
||
<a id="terms-export-btn" href="lab_terminos_export.php" target="_blank" class="btn btn-sm btn-outline-success">
|
||
<i class="fas fa-file-csv me-1"></i>CSV
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<!-- Stats row -->
|
||
<div class="d-flex gap-0 border-bottom" id="terms-stats-row">
|
||
<div class="flex-fill text-center py-3 border-end">
|
||
<div class="h4 mb-0 text-success fw-bold" id="terms-stat-aceptado">—</div>
|
||
<div class="text-muted" style="font-size:.8rem;">Aceptaron</div>
|
||
</div>
|
||
<div class="flex-fill text-center py-3 border-end">
|
||
<div class="h4 mb-0 text-danger fw-bold" id="terms-stat-rechazado">—</div>
|
||
<div class="text-muted" style="font-size:.8rem;">Rechazaron</div>
|
||
</div>
|
||
<div class="flex-fill text-center py-3 border-end">
|
||
<div class="h4 mb-0 text-warning fw-bold" id="terms-stat-pendiente">—</div>
|
||
<div class="text-muted" style="font-size:.8rem;">Pendientes</div>
|
||
</div>
|
||
<div class="flex-fill text-center py-3">
|
||
<div class="h4 mb-0 fw-bold" id="terms-stat-total">—</div>
|
||
<div class="text-muted" style="font-size:.8rem;">Total</div>
|
||
</div>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-hover table-sm mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Teléfono</th>
|
||
<th>Nombre</th>
|
||
<th>Estado</th>
|
||
<th>Versión T&C</th>
|
||
<th>Fecha envío</th>
|
||
<th>Fecha respuesta</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="terms-table-body">
|
||
<tr><td colspan="7" class="text-center text-muted py-4">Haz clic en <i class="fas fa-search"></i> para cargar</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<!-- Paginación -->
|
||
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top" id="terms-pagination-row" style="display:none!important;">
|
||
<small class="text-muted" id="terms-pagination-info"></small>
|
||
<div class="d-flex gap-1" id="terms-pagination-btns"></div>
|
||
</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>
|
||
|
||
<!-- Los modales de usuario se centralizaron en lab_usuarios.php -->
|
||
|
||
<!-- 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');
|
||
|
||
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:', 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');
|
||
|
||
updateRateLimitUI(result.data);
|
||
|
||
} catch (error) {
|
||
console.error('Error cargando rate limit stats:', error);
|
||
const statusEl = document.getElementById('rate-limit-status');
|
||
if (statusEl) {
|
||
statusEl.className = 'alert alert-warning';
|
||
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Error cargando estadísticas: ' + error.message;
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateRateLimitUI(stats) {
|
||
const statusEl = document.getElementById('rate-limit-status');
|
||
if (!statusEl) return;
|
||
|
||
// Redis no disponible
|
||
if (stats.status === 'unavailable' || stats.error) {
|
||
statusEl.className = 'alert alert-info';
|
||
statusEl.innerHTML = '<i class="fas fa-info-circle"></i> Redis no disponible. Los datos de Facebook API se capturarán en la próxima llamada. <small class="d-block text-muted mt-1">' + (stats.error || '') + '</small>';
|
||
setProgressBarsEmpty();
|
||
setLocalLimits(stats.local_limits);
|
||
return;
|
||
}
|
||
|
||
// Status general
|
||
let statusClass = 'alert-success', statusIcon = 'fa-check-circle', 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 de Facebook';
|
||
}
|
||
statusEl.className = 'alert ' + statusClass;
|
||
statusEl.innerHTML = '<i class="fas ' + statusIcon + '"></i> ' + statusText +
|
||
(stats.datetime ? ' <small class="float-end text-muted">Actualizado: ' + stats.datetime + '</small>' : '');
|
||
|
||
// Métricas 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 {
|
||
setProgressBarsEmpty();
|
||
}
|
||
|
||
// Límites locales
|
||
setLocalLimits(stats.local_limits);
|
||
|
||
// Alertas recientes
|
||
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 => {
|
||
if (!alert) return;
|
||
const cls = alert.severity === 'critical' ? 'list-group-item-danger' : 'list-group-item-warning';
|
||
const icon = alert.severity === 'critical' ? 'fa-exclamation-triangle' : 'fa-exclamation-circle';
|
||
const el = document.createElement('div');
|
||
el.className = 'list-group-item ' + cls;
|
||
el.innerHTML = `<div class="d-flex w-100 justify-content-between">
|
||
<h6 class="mb-1"><i class="fas ${icon}"></i> Alerta de ${alert.level || '—'}</h6>
|
||
<small>${alert.datetime || ''}</small>
|
||
</div>
|
||
<p class="mb-1">Uso al ${alert.usage || 0}% — Endpoint: ${(alert.data && alert.data.endpoint) || 'N/A'}</p>`;
|
||
alertsContainer.appendChild(el);
|
||
});
|
||
} else if (alertsSection) {
|
||
alertsSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function setProgressBarsEmpty() {
|
||
['fb-call-count-bar','fb-total-time-bar','fb-cpu-time-bar'].forEach(id => {
|
||
const bar = document.getElementById(id);
|
||
if (!bar) return;
|
||
bar.style.width = '0%';
|
||
bar.textContent = 'Sin datos';
|
||
bar.className = 'progress-bar bg-secondary';
|
||
});
|
||
}
|
||
|
||
function setLocalLimits(limits) {
|
||
if (!limits) return;
|
||
const map = {
|
||
'local-limit-second-max': limits.per_second,
|
||
'local-limit-hour-max': limits.per_hour,
|
||
'local-limit-day-max': limits.per_day,
|
||
};
|
||
Object.entries(map).forEach(([id, val]) => {
|
||
const el = document.getElementById(id);
|
||
if (el && val !== undefined) el.textContent = val.toLocaleString();
|
||
});
|
||
// Limpiar guiones de contadores actuales
|
||
['local-limit-second','local-limit-hour','local-limit-day'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el && el.textContent === '-') el.textContent = '—';
|
||
});
|
||
}
|
||
|
||
function updateProgressBar(id, percentage) {
|
||
const bar = document.getElementById(id);
|
||
if (!bar) return;
|
||
percentage = Math.min(100, Math.max(0, parseFloat(percentage) || 0));
|
||
bar.style.width = percentage + '%';
|
||
bar.textContent = percentage.toFixed(1) + '%';
|
||
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 al iniciar
|
||
document.addEventListener('DOMContentLoaded', refreshRateLimitStats);
|
||
|
||
// Recargar al activar el tab dashboard
|
||
document.querySelectorAll('.nav-link[data-tab="dashboard"]').forEach(link => {
|
||
link.addEventListener('click', () => 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>
|
||
// NotificationManager eliminado: las notificaciones se manejan
|
||
// vía SSE en conversations.php. Esta página no tiene campana de notificaciones.
|
||
const NotificationManager = { init() {} };
|
||
|
||
// ====== 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');
|
||
|
||
// Detectar header de imagen
|
||
const hasImageHeader = (template.header_type || '').toUpperCase() === 'IMAGE';
|
||
let html = '';
|
||
|
||
if (hasImageHeader) {
|
||
html += `
|
||
<div class="mb-3 border rounded p-2 bg-light" id="bc-image-header-section">
|
||
<label class="form-label fw-bold text-primary"><i class="fas fa-image me-1"></i> Imagen del encabezado</label>
|
||
<p class="text-muted small mb-2">Esta plantilla requiere una imagen de encabezado dinámica. Súbela aquí.</p>
|
||
<input type="file" class="form-control" id="broadcast-template-image" accept="image/jpeg,image/png,image/webp" required>
|
||
<div id="bc-image-preview" class="mt-2" style="display:none">
|
||
<img id="bc-image-thumb" src="" style="max-height:100px;border-radius:6px;" class="border">
|
||
</div>
|
||
</div>
|
||
`;
|
||
setTimeout(() => {
|
||
const imgInput = document.getElementById('broadcast-template-image');
|
||
if (imgInput) imgInput.addEventListener('change', function() {
|
||
const file = this.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = e => {
|
||
document.getElementById('bc-image-thumb').src = e.target.result;
|
||
document.getElementById('bc-image-preview').style.display = 'block';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
}, 50);
|
||
}
|
||
|
||
if (template.variables && template.variables.length > 0) {
|
||
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
||
html += `<label class="form-label fw-bold${hasImageHeader ? ' mt-2' : ''}"><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 {
|
||
if (!hasImageHeader) html += '<p class="text-muted small">Esta plantilla no tiene variables de texto</p>';
|
||
console.log('⚠️ Template sin variables');
|
||
variablesContainer.innerHTML = html;
|
||
variablesContainer.style.display = (hasImageHeader || html) ? 'block' : 'none';
|
||
}
|
||
|
||
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>
|
||
|
||
<!-- ── T&C Aceptaciones JS ───────────────────────────────────────────── -->
|
||
<script>
|
||
const termsAdmin = (() => {
|
||
let _page = 1;
|
||
const perPage = 50;
|
||
|
||
function estadoBadge(e) {
|
||
const map = {
|
||
aceptado: '<span class="badge bg-success">Aceptado</span>',
|
||
rechazado: '<span class="badge bg-danger">Rechazado</span>',
|
||
pendiente: '<span class="badge bg-warning text-dark">Pendiente</span>',
|
||
};
|
||
return map[e] || `<span class="badge bg-secondary">${e}</span>`;
|
||
}
|
||
|
||
function formatDate(d) {
|
||
if (!d) return '—';
|
||
const dt = new Date(d.replace(' ', 'T'));
|
||
return dt.toLocaleString('es-CO', { dateStyle: 'short', timeStyle: 'short' });
|
||
}
|
||
|
||
async function cargar(page) {
|
||
_page = page || 1;
|
||
const estado = document.getElementById('terms-filter-estado')?.value || '';
|
||
const fecha = document.getElementById('terms-filter-fecha')?.value || '';
|
||
const phone = document.getElementById('terms-filter-phone')?.value || '';
|
||
|
||
const params = new URLSearchParams({ page: _page, per_page: perPage });
|
||
if (estado) params.set('estado', estado);
|
||
if (fecha) params.set('fecha', fecha);
|
||
if (phone) params.set('phone', phone);
|
||
|
||
// Actualizar link de exportación
|
||
const expBtn = document.getElementById('terms-export-btn');
|
||
if (expBtn) {
|
||
const ep = new URLSearchParams();
|
||
if (estado) ep.set('estado', estado);
|
||
if (fecha) ep.set('fecha', fecha);
|
||
if (phone) ep.set('phone', phone);
|
||
expBtn.href = 'lab_terminos_export.php?' + ep.toString();
|
||
}
|
||
|
||
const tbody = document.getElementById('terms-table-body');
|
||
if (tbody) tbody.innerHTML = '<tr><td colspan="7" class="text-center py-3"><div class="spinner-border spinner-border-sm text-primary"></div></td></tr>';
|
||
|
||
try {
|
||
const res = await fetch('api/get_terms_acceptances.php?' + params.toString());
|
||
const json = await res.json();
|
||
|
||
if (!json.success) throw new Error(json.error || 'Error al cargar');
|
||
|
||
const rows = json.data || [];
|
||
const stats = json.stats || {};
|
||
const total = json.total || 0;
|
||
const pages = Math.ceil(total / perPage);
|
||
|
||
// Stats
|
||
document.getElementById('terms-stat-aceptado').textContent = stats.aceptado ?? 0;
|
||
document.getElementById('terms-stat-rechazado').textContent = stats.rechazado ?? 0;
|
||
document.getElementById('terms-stat-pendiente').textContent = stats.pendiente ?? 0;
|
||
document.getElementById('terms-stat-total').textContent = total;
|
||
|
||
// Tabla
|
||
if (!rows.length) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">Sin registros</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = rows.map(r => `
|
||
<tr>
|
||
<td class="text-muted" style="font-size:.8rem;">${r.id}</td>
|
||
<td><code style="font-size:.8rem;">${r.phone_number}</code></td>
|
||
<td>${r.user_name || '<span class="text-muted">—</span>'}</td>
|
||
<td>${estadoBadge(r.estado)}</td>
|
||
<td><span class="badge bg-secondary">${r.terms_version || '—'}</span></td>
|
||
<td style="font-size:.8rem;">${formatDate(r.fecha_envio)}</td>
|
||
<td style="font-size:.8rem;">${formatDate(r.fecha_respuesta)}</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
// Paginación
|
||
const pInfo = document.getElementById('terms-pagination-info');
|
||
const pBtns = document.getElementById('terms-pagination-btns');
|
||
const pRow = document.getElementById('terms-pagination-row');
|
||
if (pRow) pRow.style.display = pages > 1 ? 'flex' : 'none';
|
||
if (pInfo) pInfo.textContent = `Página ${_page} de ${pages} (${total} registros)`;
|
||
if (pBtns) {
|
||
pBtns.innerHTML = '';
|
||
for (let p = 1; p <= Math.min(pages, 10); p++) {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'btn btn-sm ' + (p === _page ? 'btn-primary' : 'btn-outline-secondary');
|
||
btn.textContent = p;
|
||
btn.onclick = () => cargar(p);
|
||
pBtns.appendChild(btn);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('termsAdmin.cargar error', e);
|
||
if (tbody) tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger py-3">${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// Cargar cuando se activa el tab
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const link = document.querySelector('.nav-link[data-tab="terms"]');
|
||
if (link) link.addEventListener('click', () => setTimeout(() => cargar(1), 50));
|
||
});
|
||
|
||
return { cargar };
|
||
})();
|
||
</script>
|
||
<!-- ── / T&C Aceptaciones JS ─────────────────────────────────────────── -->
|
||
</body>
|
||
|
||
</html>
|