Files
whatsapp/index.php
T

2847 lines
158 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>');
}
// Redirigir roles que no deben ver el panel WhatsApp
if (isEnfermero()) {
header('Location: enfermero_portal.php'); exit;
}
$_indexRole = $_SESSION['admin_user']['role'] ?? '';
$_indexModules = $_SESSION['admin_user']['modules'] ?? [];
if ($_indexRole === 'recepcionista') {
header('Location: erp.php?m=turnero&v=recepcion'); exit;
}
if (in_array('turnero', $_indexModules, true) && !in_array('whatsapp', $_indexModules, true)) {
$_indexAdminRoles = ['superadmin', 'admin', 'supervisor', 'bacteriologo'];
$url = in_array($_indexRole, $_indexAdminRoles, true)
? 'erp.php?m=turnero&v=dashboard'
: 'erp.php?m=turnero&v=recepcion';
header('Location: ' . $url); exit;
}
if (in_array('lab_dashboard', $_indexModules, true) && !in_array('whatsapp', $_indexModules, true)) {
header('Location: lab_dashboard.php'); exit;
}
// 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&amp;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 style="min-width:0;flex:1">
<h1><i class="fas fa-robot d-none d-sm-inline"></i> <span class="d-none d-sm-inline">Sistema de Gestión</span> WhatsApp Bot</h1>
<small class="text-muted d-none d-md-block">
<i class="fas fa-user"></i> Conectado como <strong><?= htmlspecialchars(
$_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 me-1" title="Abrir menú" style="display:none">
<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 btn-sm d-none d-sm-inline-flex align-items-center gap-1" onclick="refreshData()">
<i class="fas fa-sync-alt"></i> <span class="d-none d-lg-inline">Actualizar</span>
</button>
<a href="logout.php" class="btn btn-outline-danger btn-sm d-none d-sm-inline-flex align-items-center gap-1" onclick="return confirm('¿Está seguro que desea cerrar sesión?')">
<i class="fas fa-sign-out-alt"></i> <span class="d-none d-lg-inline">Cerrar Sesión</span>
</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>
<!-- ══ KPI: Actividad de Pacientes ══════════════════════════════ -->
<div class="row mt-4" id="kpi-actividad-section">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex flex-wrap align-items-center gap-2 justify-content-between">
<h5 class="mb-0"><i class="fas fa-chart-bar text-primary me-2"></i>Actividad de Pacientes — KPI</h5>
<div class="d-flex flex-wrap gap-2 align-items-center">
<!-- Rango rápido -->
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary kpi-quick" data-days="7">7 días</button>
<button class="btn btn-secondary kpi-quick active" data-days="30">30 días</button>
<button class="btn btn-outline-secondary kpi-quick" data-days="90">90 días</button>
</div>
<!-- Rango personalizado -->
<input type="date" id="kpi-desde" class="form-control form-control-sm" style="width:140px">
<span class="text-muted small"></span>
<input type="date" id="kpi-hasta" class="form-control form-control-sm" style="width:140px">
<button class="btn btn-primary btn-sm" onclick="cargarKPI()">
<i class="fas fa-filter me-1"></i>Filtrar
</button>
</div>
</div>
<div class="card-body">
<!-- Tarjetas de totales -->
<div class="row g-3 mb-4" id="kpi-cards">
<div class="col-6 col-md-3">
<div class="card border-0 bg-primary bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-primary" id="kpi-total-usuarios">—</div>
<div class="small text-muted mt-1"><i class="fas fa-users me-1"></i>Usuarios únicos</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-success bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-success" id="kpi-total-mensajes">—</div>
<div class="small text-muted mt-1"><i class="fab fa-whatsapp me-1"></i>Mensajes recibidos</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-warning bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-warning" id="kpi-total-agend">—</div>
<div class="small text-muted mt-1"><i class="fas fa-calendar-check me-1"></i>Agendamientos</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-info bg-opacity-10 text-center py-3">
<div class="fs-4 fw-bold text-info" id="kpi-pico-dia">—</div>
<div class="small text-muted mt-1"><i class="fas fa-trophy me-1"></i>Día más activo</div>
</div>
</div>
</div>
<!-- Gráficas -->
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-calendar-alt me-1"></i>Por día</h6>
<div style="position:relative; height:220px">
<canvas id="kpiChartDia"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-calendar-week me-1"></i>Por día de la semana</h6>
<div style="position:relative; height:220px">
<canvas id="kpiChartSemana"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════ -->
<!-- ══ KPI: Tiempo de Respuesta de Asesores ════════════════════ -->
<div class="row mt-4" id="kpi-asesor-section">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex flex-wrap align-items-center gap-2 justify-content-between">
<h5 class="mb-0"><i class="fas fa-stopwatch text-danger me-2"></i>Tiempo de Respuesta de Asesores</h5>
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary kpi-asesor-quick" data-days="7">7 días</button>
<button class="btn btn-secondary kpi-asesor-quick active" data-days="30">30 días</button>
<button class="btn btn-outline-secondary kpi-asesor-quick" data-days="90">90 días</button>
</div>
<input type="date" id="kpia-desde" class="form-control form-control-sm" style="width:140px">
<span class="text-muted small"></span>
<input type="date" id="kpia-hasta" class="form-control form-control-sm" style="width:140px">
<button class="btn btn-danger btn-sm" onclick="cargarKPIAsesor()">
<i class="fas fa-filter me-1"></i>Filtrar
</button>
</div>
</div>
<div class="card-body">
<!-- Tarjetas de totales -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-2">
<div class="card border-0 bg-danger bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-danger" id="kpia-total-sesiones">—</div>
<div class="small text-muted mt-1"><i class="fas fa-headset me-1"></i>Atenciones</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-warning bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-warning" id="kpia-prom-resp">—</div>
<div class="small text-muted mt-1"><i class="fas fa-clock me-1"></i>Prom. 1ª respuesta</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-info bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-info" id="kpia-prom-dur">—</div>
<div class="small text-muted mt-1"><i class="fas fa-hourglass-half me-1"></i>Prom. duración</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-success bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-success" id="kpia-pct-rapidas">—</div>
<div class="small text-muted mt-1"><i class="fas fa-bolt me-1"></i>Resp. &lt; 3 min</div>
</div>
</div>
<div class="col-6 col-md-2">
<div class="card border-0 bg-secondary bg-opacity-10 text-center py-3">
<div class="fs-2 fw-bold text-secondary" id="kpia-sin-resp">—</div>
<div class="small text-muted mt-1"><i class="fas fa-comment-slash me-1"></i>Sin respuesta</div>
</div>
</div>
</div>
<!-- Gráficas -->
<div class="row g-3 mb-3">
<div class="col-lg-8">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-chart-line me-1"></i>Promedio de respuesta por día (minutos)</h6>
<div style="position:relative; height:200px">
<canvas id="kpiaChartDia"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 bg-light">
<div class="card-body">
<h6 class="text-muted mb-3"><i class="fas fa-chart-pie me-1"></i>Distribución de tiempos</h6>
<div style="position:relative; height:200px">
<canvas id="kpiaChartHist"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla por asesor -->
<div class="card border-0 bg-light">
<div class="card-body p-0">
<h6 class="text-muted mb-0 px-3 pt-3"><i class="fas fa-user-tie me-1"></i>Detalle por asesor</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 mt-2" id="kpia-tabla-asesores">
<thead class="table-light">
<tr>
<th>Asesor</th>
<th class="text-center">Atenciones</th>
<th class="text-center">Prom. 1ª respuesta</th>
<th class="text-center">Prom. duración</th>
<th>Velocidad</th>
</tr>
</thead>
<tbody id="kpia-body-asesores">
<tr><td colspan="5" class="text-center text-muted py-3"><i class="fas fa-spinner fa-spin"></i></td></tr>
</tbody>
</table>
</div>
</div>
</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 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 &amp; 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 &amp; 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>
<!-- Historial de Envíos Masivos -->
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-history me-2 text-secondary"></i>Historial de Envíos Masivos</h5>
<button class="btn btn-sm btn-outline-secondary" onclick="loadBroadcastHistory(1)">
<i class="fas fa-sync-alt"></i> Actualizar
</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-sm mb-0">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th>Tipo</th>
<th>Mensaje / Plantilla</th>
<th class="text-center">Total</th>
<th class="text-center text-success">Enviados</th>
<th class="text-center text-danger">Errores</th>
<th>Enviado por</th>
</tr>
</thead>
<tbody id="broadcast-history-tbody">
<tr>
<td colspan="7" class="text-center text-muted py-4">
<i class="fas fa-spinner fa-spin me-1"></i> Cargando historial...
</td>
</tr>
</tbody>
</table>
</div>
<div id="bh-pagination-row" class="d-flex justify-content-between align-items-center px-3 py-2 border-top" style="display:none!important">
<small class="text-muted" id="bh-pagination-info"></small>
<div class="d-flex gap-1" id="bh-pagination-btns"></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&amp;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="row">
<div class="col-md-6">
<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>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Tipo de Menú</label>
<select class="form-control" id="menu-type">
<option value="main">Principal (normal)</option>
<option value="no_advisor">🔴 Asesor ausente (secundario)</option>
</select>
<small class="text-muted">El menú <em>Asesor ausente</em> se muestra cuando el asesor está marcado como no disponible.</small>
</div>
</div>
</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 ─────────────────────────────────────────── -->
<!-- ── Historial Envíos Masivos JS ──────────────────────────────────── -->
<script>
(function () {
let _currentPage = 1;
window.loadBroadcastHistory = function (page) {
page = page || 1;
_currentPage = page;
const tbody = document.getElementById('broadcast-history-tbody');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3"><i class="fas fa-spinner fa-spin me-1"></i> Cargando...</td></tr>';
fetch('api/get_broadcast_history.php?page=' + page + '&per_page=10')
.then(r => r.json())
.then(res => {
if (!res.success || !res.data) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">Sin registros</td></tr>';
return;
}
if (res.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3"><i class="fas fa-inbox me-1"></i> Aún no hay envíos masivos registrados</td></tr>';
} else {
const typeLabels = { text: 'Texto', template: 'Plantilla', image: 'Imagen', document: 'Documento' };
const filterLabels = { all: 'Todos', active: 'Activos', recent: 'Recientes', null: '—' };
tbody.innerHTML = res.data.map(r => {
const fecha = r.sent_at ? r.sent_at.replace('T', ' ').substring(0, 16) : '—';
const tipo = typeLabels[r.message_type] || r.message_type || '—';
const msg = r.template_name
? '<span class="badge bg-success-subtle text-success border border-success-subtle me-1">Plantilla</span>' + htmlEsc(r.template_name)
: (r.message_preview ? htmlEsc(r.message_preview.substring(0, 60)) + (r.message_preview.length > 60 ? '…' : '') : '—');
const pct = r.total_users > 0 ? Math.round(r.sent_count / r.total_users * 100) : 0;
return `<tr>
<td class="small text-nowrap">${fecha}</td>
<td><span class="badge bg-secondary-subtle text-secondary border">${tipo}</span></td>
<td class="small">${msg}</td>
<td class="text-center fw-bold">${r.total_users}</td>
<td class="text-center text-success fw-bold">${r.sent_count} <small class="text-muted">(${pct}%)</small></td>
<td class="text-center ${r.error_count > 0 ? 'text-danger fw-bold' : 'text-muted'}">${r.error_count}</td>
<td class="small text-muted">${htmlEsc(r.sent_by_name || 'Sistema')}</td>
</tr>`;
}).join('');
}
// Paginación
const pagRow = document.getElementById('bh-pagination-row');
const pagInfo = document.getElementById('bh-pagination-info');
const pagBtns = document.getElementById('bh-pagination-btns');
if (pagRow && res.pages > 1) {
pagRow.style.display = '';
pagInfo.textContent = 'Página ' + res.page + ' de ' + res.pages + ' (' + res.total + ' registros)';
let btns = '';
for (let p = 1; p <= res.pages; p++) {
btns += `<button class="btn btn-sm ${p === res.page ? 'btn-secondary' : 'btn-outline-secondary'}" onclick="loadBroadcastHistory(${p})">${p}</button>`;
}
pagBtns.innerHTML = btns;
} else if (pagRow) {
pagRow.style.display = 'none';
}
})
.catch(err => {
if (tbody) tbody.innerHTML = '<tr><td colspan="7" class="text-center text-danger py-3">Error al cargar el historial</td></tr>';
console.error('loadBroadcastHistory error:', err);
});
};
function htmlEsc(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Cargar al activar el tab de broadcast
document.addEventListener('DOMContentLoaded', () => {
const broadcastLinks = document.querySelectorAll('.nav-link[data-tab="broadcast"], [data-tab="broadcast"]');
broadcastLinks.forEach(link => {
link.addEventListener('click', () => setTimeout(() => loadBroadcastHistory(1), 100));
});
});
})();
</script>
<!-- ── KPI Actividad de Pacientes JS ────────────────────────────────── -->
<script>
(function () {
'use strict';
let chartDia = null;
let chartSemana = null;
const fmt = n => Number(n).toLocaleString('es-CO');
// ── Inicializar fechas en los inputs ──────────────────────────────
function initFechas(days = 30) {
const hoy = new Date();
const desde = new Date(hoy); desde.setDate(hoy.getDate() - (days - 1));
document.getElementById('kpi-hasta').value = hoy.toISOString().slice(0, 10);
document.getElementById('kpi-desde').value = desde.toISOString().slice(0, 10);
}
// ── Carga y renderiza ─────────────────────────────────────────────
window.cargarKPI = async function () {
const desde = document.getElementById('kpi-desde').value;
const hasta = document.getElementById('kpi-hasta').value;
if (!desde || !hasta) return;
try {
const r = await fetch(`api/kpi_actividad.php?desde=${desde}&hasta=${hasta}`);
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Error');
// Tarjetas
document.getElementById('kpi-total-usuarios').textContent = fmt(d.totales.usuarios);
document.getElementById('kpi-total-mensajes').textContent = fmt(d.totales.mensajes);
document.getElementById('kpi-total-agend').textContent = fmt(d.totales.agendamientos);
document.getElementById('kpi-pico-dia').textContent = d.totales.pico_semana;
// Gráfica por día
const labsDia = d.por_dia.map(x => {
const f = new Date(x.fecha + 'T00:00:00');
return f.toLocaleDateString('es-CO', { day:'2-digit', month:'short' });
});
const dataUsuarios = d.por_dia.map(x => x.usuarios);
const dataAgend = d.por_dia.map(x => x.agendamientos);
if (chartDia) chartDia.destroy();
chartDia = new Chart(document.getElementById('kpiChartDia'), {
type: 'bar',
data: {
labels: labsDia,
datasets: [
{
label: 'Usuarios que escriben',
data: dataUsuarios,
backgroundColor: 'rgba(13,110,253,0.6)',
borderRadius: 4,
order: 1,
},
{
label: 'Agendamientos',
data: dataAgend,
backgroundColor: 'rgba(255,193,7,0.75)',
borderRadius: 4,
order: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
x: { ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { precision: 0 } },
},
},
});
// Gráfica por día de semana
const labsSem = d.por_dia_semana.map(x => x.dia);
const dataSemU = d.por_dia_semana.map(x => x.usuarios);
const dataSemA = d.por_dia_semana.map(x => x.agendamientos);
if (chartSemana) chartSemana.destroy();
chartSemana = new Chart(document.getElementById('kpiChartSemana'), {
type: 'bar',
data: {
labels: labsSem,
datasets: [
{
label: 'Usuarios',
data: dataSemU,
backgroundColor: 'rgba(13,110,253,0.6)',
borderRadius: 4,
},
{
label: 'Agendamientos',
data: dataSemA,
backgroundColor: 'rgba(255,193,7,0.75)',
borderRadius: 4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
y: { beginAtZero: true, ticks: { precision: 0 } },
},
},
});
} catch (e) {
console.error('KPI error:', e);
}
};
// ── Botones de rango rápido ───────────────────────────────────────
document.querySelectorAll('.kpi-quick').forEach(btn => {
btn.addEventListener('click', function () {
document.querySelectorAll('.kpi-quick').forEach(b => {
b.classList.remove('btn-secondary', 'active');
b.classList.add('btn-outline-secondary');
});
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-secondary', 'active');
initFechas(parseInt(this.dataset.days, 10));
cargarKPI();
});
});
// ── Carga inicial (30 días por defecto) ───────────────────────────
// Solo se ejecuta cuando la pestaña dashboard está visible
function kpiInit() {
initFechas(30);
cargarKPI();
}
// Esperar a que el tab dashboard esté activo
const observer = new MutationObserver(() => {
const sec = document.getElementById('kpi-actividad-section');
if (sec && sec.closest('.tab-content')?.style.display !== 'none') {
kpiInit();
observer.disconnect();
}
});
// Si la sección ya es visible al cargar, inicializar de inmediato
document.addEventListener('DOMContentLoaded', () => {
const sec = document.getElementById('kpi-actividad-section');
if (sec) kpiInit();
});
})();
</script>
<!-- ── KPI Tiempo de Respuesta Asesores JS ──────────────────────────── -->
<script>
(function () {
'use strict';
let chartDiaA = null;
let chartHistA = null;
const fmtMin = m => m === null ? '—' : (m < 60 ? m + ' min' : Math.floor(m/60) + 'h ' + (m%60) + 'm');
const escA = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
function initFechasA(days = 30) {
const hoy = new Date();
const desde = new Date(hoy); desde.setDate(hoy.getDate() - (days - 1));
document.getElementById('kpia-hasta').value = hoy.toISOString().slice(0, 10);
document.getElementById('kpia-desde').value = desde.toISOString().slice(0, 10);
}
function velocidadBadge(min) {
if (min === null) return '<span class="badge bg-secondary">Sin datos</span>';
if (min < 3) return '<span class="badge bg-success">Muy rápido</span>';
if (min < 10) return '<span class="badge bg-primary">Normal</span>';
if (min < 20) return '<span class="badge bg-warning text-dark">Lento</span>';
return '<span class="badge bg-danger">Muy lento</span>';
}
window.cargarKPIAsesor = async function () {
const desde = document.getElementById('kpia-desde').value;
const hasta = document.getElementById('kpia-hasta').value;
if (!desde || !hasta) return;
try {
const r = await fetch(`api/kpi_respuesta_asesor.php?desde=${desde}&hasta=${hasta}`);
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Error');
const t = d.totales;
document.getElementById('kpia-total-sesiones').textContent = t.sesiones;
document.getElementById('kpia-prom-resp').textContent = fmtMin(t.prom_respuesta);
document.getElementById('kpia-prom-dur').textContent = fmtMin(t.prom_duracion);
document.getElementById('kpia-pct-rapidas').textContent = t.pct_rapidas + '%';
document.getElementById('kpia-sin-resp').textContent = t.sin_respuesta;
// Gráfica por día
const labsDia = d.serie_dia.map(x => {
const f = new Date(x.fecha + 'T00:00:00');
return f.toLocaleDateString('es-CO', { day:'2-digit', month:'short' });
});
const dataResp = d.serie_dia.map(x => x.prom_respuesta);
const dataSes = d.serie_dia.map(x => x.sesiones);
if (chartDiaA) chartDiaA.destroy();
chartDiaA = new Chart(document.getElementById('kpiaChartDia'), {
data: {
labels: labsDia,
datasets: [
{
type: 'line',
label: 'Prom. respuesta (min)',
data: dataResp,
borderColor: 'rgba(220,53,69,0.9)',
backgroundColor: 'rgba(220,53,69,0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
yAxisID: 'y',
},
{
type: 'bar',
label: 'Atenciones',
data: dataSes,
backgroundColor: 'rgba(108,117,125,0.3)',
borderRadius: 3,
yAxisID: 'y2',
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
x: { ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { precision: 0 }, title: { display: true, text: 'Min' } },
y2: { beginAtZero: true, position: 'right', grid: { drawOnChartArea: false }, ticks: { precision: 0 }, title: { display: true, text: 'Atenciones' } },
},
},
});
// Histograma de distribución
const labsH = d.histograma.map(x => x.rango);
const dataH = d.histograma.map(x => x.sesiones);
const colores = [
'rgba(25,135,84,0.7)', // < 1 min verde
'rgba(13,110,253,0.7)', // 1-3 min azul
'rgba(255,193,7,0.7)', // 3-5 min amarillo
'rgba(253,126,20,0.7)', // 5-10 min naranja
'rgba(220,53,69,0.7)', // 10-20 rojo
'rgba(108,17,17,0.7)', // >20 rojo oscuro
'rgba(108,117,125,0.7)', // Sin resp gris
];
if (chartHistA) chartHistA.destroy();
chartHistA = new Chart(document.getElementById('kpiaChartHist'), {
type: 'doughnut',
data: {
labels: labsH,
datasets: [{ data: dataH, backgroundColor: colores, borderWidth: 1 }],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { font: { size: 11 } } },
},
},
});
// Tabla de asesores
const tbody = document.getElementById('kpia-body-asesores');
if (!d.asesores.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-2">Sin datos</td></tr>';
} else {
tbody.innerHTML = d.asesores.map(a => `
<tr>
<td class="fw-semibold">${escA(a.operador)}</td>
<td class="text-center">${a.sesiones}</td>
<td class="text-center">${fmtMin(a.prom_respuesta)}</td>
<td class="text-center">${fmtMin(a.prom_duracion)}</td>
<td>${velocidadBadge(a.prom_respuesta)}</td>
</tr>
`).join('');
}
} catch (e) {
console.error('KPI Asesores error:', e);
}
};
// Botones rápidos
document.querySelectorAll('.kpi-asesor-quick').forEach(btn => {
btn.addEventListener('click', function () {
document.querySelectorAll('.kpi-asesor-quick').forEach(b => {
b.classList.remove('btn-secondary', 'active');
b.classList.add('btn-outline-secondary');
});
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-secondary', 'active');
initFechasA(parseInt(this.dataset.days, 10));
cargarKPIAsesor();
});
});
// Carga inicial
document.addEventListener('DOMContentLoaded', () => {
const sec = document.getElementById('kpi-asesor-section');
if (sec) { initFechasA(30); cargarKPIAsesor(); }
});
})();
</script>
<!-- ── / KPI Tiempo de Respuesta Asesores JS ─────────────────────────── -->
</body>
</html>