BD: home_page asignado a roles que lo tenían NULL: - recepcionista → /erp.php?m=turnero&v=recepcion - supervisor → /lab_dashboard.php - operador_bot → /index.php - readonly → /lab_ordenes.php login.php: fallback mejorado por módulos cuando home_page no aplica (turnero → recepcion, lab_dashboard → dashboard, etc.) index.php: redirige recepcionistas y roles sin whatsapp a su página correcta si por algún motivo llegan al panel de WhatsApp. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2840 lines
158 KiB
PHP
2840 lines
158 KiB
PHP
<?php
|
||
|
||
/**
|
||
* Página principal del sistema WhatsApp Bot
|
||
* Desarrollado por U-Site.app
|
||
*/
|
||
|
||
require_once 'config/config.php';
|
||
|
||
// Verificar autenticación - redirigir al login si no está logueado
|
||
if (!isUserLoggedIn()) {
|
||
header('Location: login.php');
|
||
exit('Acceso denegado. <a href="login.php">Iniciar sesión</a>');
|
||
}
|
||
|
||
// 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' || (in_array('turnero', $_indexModules, true) && !in_array('whatsapp', $_indexModules, true))) {
|
||
header('Location: erp.php?m=turnero&v=recepcion'); 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&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. < 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> Conversaciones
|
||
<a href="conversations.php" class="btn btn-sm btn-light ms-3" style="font-size:0.75rem;">Ver nueva versión</a>
|
||
</h5>
|
||
<small class="text-white-50">Administra tus chats de WhatsApp</small>
|
||
</div>
|
||
<div class="col-auto d-flex align-items-center gap-2">
|
||
<div class="input-group">
|
||
<span class="input-group-text bg-light border-0">
|
||
<i class="fas fa-search"></i>
|
||
</span>
|
||
<input type="text" class="form-control border-0"
|
||
placeholder="Buscar conversaciones..."
|
||
id="search-conversations">
|
||
</div>
|
||
<button class="btn btn-light btn-sm text-nowrap" onclick="exportConversations()" title="Exportar todas las conversaciones a CSV">
|
||
<i class="fas fa-file-csv"></i> Exportar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="conversation-list" id="conversations-container">
|
||
<!-- Contenido dinámico -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Users Tab -->
|
||
<div id="users" class="tab-content">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5><i class="fas fa-users"></i> Gestión de Usuarios</h5>
|
||
<button class="btn btn-primary" onclick="exportUsers()">
|
||
<i class="fas fa-download"></i> Exportar
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- Búsqueda de usuarios -->
|
||
<div class="mb-3">
|
||
<div class="input-group">
|
||
<span class="input-group-text">
|
||
<i class="fas fa-search"></i>
|
||
</span>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="users-search"
|
||
placeholder="Buscar por teléfono, nombre o email..."
|
||
onkeyup="searchUsers()"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Teléfono</th>
|
||
<th>Nombre</th>
|
||
<th>Estado</th>
|
||
<th>Menú Actual</th>
|
||
<th>Registro</th>
|
||
<th>Acciones</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="users-table">
|
||
<!-- Contenido dinámico -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Paginación -->
|
||
<div id="users-pagination"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Menus Tab -->
|
||
<div id="menus" class="tab-content">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5><i class="fas fa-list"></i> Menús Configurados</h5>
|
||
<button class="btn btn-primary" onclick="openCreateMenuModal()">
|
||
<i class="fas fa-plus"></i> Nuevo Menú
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>Nombre</th>
|
||
<th>Clave</th>
|
||
<th>Descripción</th>
|
||
<th>Estado</th>
|
||
<th>Opciones</th>
|
||
<th>Fecha Creación</th>
|
||
<th>Acciones</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="menus-table">
|
||
<!-- Contenido dinámico -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Users Tab -->
|
||
<div id="admin_users" class="tab-content">
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-body text-center py-5">
|
||
<i class="fas fa-users-cog fa-3x text-primary mb-3"></i>
|
||
<h5 class="fw-semibold mb-2">Gestión de Usuarios & Roles</h5>
|
||
<p class="text-muted mb-4">
|
||
La administración de usuarios, roles y permisos de módulo<br>
|
||
se encuentra centralizada en el Módulo de Laboratorio.
|
||
</p>
|
||
<a href="lab_usuarios.php" class="btn btn-primary px-4">
|
||
<i class="fas fa-external-link-alt me-2"></i>Ir a Usuarios & Roles
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- conversations Tab -->
|
||
<div id="send_message" class="tab-content">
|
||
<div class="row">
|
||
<div class="col-lg-6">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5><i class="fas fa-paper-plane"></i> Enviar Mensaje</h5>
|
||
<div>
|
||
<button class="btn btn-sm btn-secondary" onclick="whatsappManager.loadconversations()">🔄 Actualizar</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="send-message-form">
|
||
<div class="mb-3">
|
||
<label class="form-label">Tipo de Destinatario</label>
|
||
<select class="form-control" id="recipient-type" onchange="toggleRecipientType()">
|
||
<option value="existing">Usuario Registrado</option>
|
||
<option value="manual">Número Manual (Solo Plantillas)</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3" id="existing-recipient-group">
|
||
<label class="form-label">Destinatario</label>
|
||
<select class="form-control" id="message-recipient">
|
||
<option value="">Seleccionar usuario...</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3" id="manual-recipient-group" style="display: none;">
|
||
<label class="form-label">Número de WhatsApp</label>
|
||
<input type="text" class="form-control" id="manual-recipient" placeholder="+57XXXXXXXXX">
|
||
<small class="form-text text-warning">
|
||
⚠️ Solo se pueden enviar plantillas aprobadas a números nuevos
|
||
</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Tipo de Mensaje</label>
|
||
<select class="form-control" id="message-type" onchange="toggleMessageType()">
|
||
<option value="text">Texto</option>
|
||
<option value="template">Plantilla</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3" id="message-text-group">
|
||
<label class="form-label">Mensaje</label>
|
||
<textarea class="form-control" id="message-text" rows="4" placeholder="Escribe tu mensaje aquí..."></textarea>
|
||
</div>
|
||
<div class="mb-3" id="template-group" style="display: none;">
|
||
<label class="form-label">Plantilla</label>
|
||
<select class="form-control" id="message-template" onchange="loadMessageTemplateDetails()">
|
||
<option value="">Seleccionar plantilla...</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Variables de la plantilla -->
|
||
<div id="message-template-variables" class="mb-3" style="display: none;">
|
||
<!-- Se llenarán dinámicamente -->
|
||
</div>
|
||
|
||
<!-- Preview de la plantilla -->
|
||
<div id="message-template-preview" class="mb-3" style="display: none;">
|
||
<label class="form-label"><i class="fas fa-eye"></i> Vista Previa</label>
|
||
<div class="card bg-light">
|
||
<div class="card-body">
|
||
<div id="message-preview-content"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-paper-plane"></i> Enviar Mensaje
|
||
</button>
|
||
<input type="hidden" name="template_params" id="template-params" value="">
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Broadcast Tab -->
|
||
<div id="broadcast" class="tab-content">
|
||
<div class="row">
|
||
<div class="col-lg-8 mx-auto">
|
||
<div class="card shadow-lg">
|
||
<div class="card-header bg-warning text-white">
|
||
<h4 class="mb-0"><i class="fas fa-broadcast-tower"></i> Envío Masivo de Mensajes</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-success mb-4">
|
||
<i class="fas fa-check-circle"></i>
|
||
<strong>¡Envío Masivo Activo!</strong> Ahora puedes enviar mensajes a múltiples usuarios simultáneamente.
|
||
</div>
|
||
|
||
<form id="broadcast-form" onsubmit="return handleBroadcastSubmit(event)">
|
||
<div class="mb-4">
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-filter"></i> Tipo de Selección
|
||
</label>
|
||
<select class="form-control form-control-lg" id="broadcast-selection-type" onchange="toggleBroadcastSelectionType()">
|
||
<option value="filter">📋 Por filtro de usuarios</option>
|
||
<option value="specific">👥 Usuarios específicos</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Filtro de usuarios -->
|
||
<div class="mb-4" id="broadcast-filter-container">
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-filter"></i> Filtro de Usuarios
|
||
</label>
|
||
<select class="form-control form-control-lg" id="broadcast-filter">
|
||
<option value="all">📋 Todos los usuarios activos</option>
|
||
<option value="active">🔥 Solo usuarios con conversaciones recientes (7 días)</option>
|
||
<option value="recent">✨ Usuarios recientes (30 días)</option>
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
<i class="fas fa-info-circle"></i> Selecciona qué usuarios recibirán el mensaje
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Selección específica de usuarios -->
|
||
<div class="mb-4" id="broadcast-users-container" style="display: none;">
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-users"></i> Seleccionar Usuarios
|
||
</label>
|
||
<select class="form-control" id="broadcast-users" multiple="multiple">
|
||
<!-- Opciones cargadas dinámicamente -->
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
<i class="fas fa-info-circle"></i> Selecciona uno o más usuarios para enviar el mensaje
|
||
</small>
|
||
</div>
|
||
|
||
<div class="mb-4">
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-envelope"></i> Tipo de Mensaje
|
||
</label>
|
||
<select class="form-control form-control-lg" id="broadcast-message-type" onchange="toggleBroadcastMessageType()">
|
||
<option value="text">📝 Mensaje de texto</option>
|
||
<option value="template">📋 Plantilla de WhatsApp</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Mensaje de texto -->
|
||
<div class="mb-4" id="broadcast-text-container">
|
||
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-comment-dots"></i> Mensaje
|
||
</label>
|
||
<textarea
|
||
class="form-control"
|
||
id="broadcast-message"
|
||
rows="6"
|
||
placeholder="Escribe el mensaje que deseas enviar a todos los usuarios...
|
||
|
||
Ejemplo:
|
||
¡Hola! 👋 Tenemos una promoción especial para ti..."
|
||
maxlength="4096"></textarea>
|
||
<div class="d-flex justify-content-between mt-2">
|
||
<small class="form-text text-muted">
|
||
<i class="fas fa-keyboard"></i> Máximo 4096 caracteres
|
||
</small>
|
||
<small class="form-text text-muted">
|
||
<span id="char-count">0</span> / 4096
|
||
</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Selección de plantilla -->
|
||
<div class="mb-4" id="broadcast-template-container" style="display: none;">
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-file-text"></i> Seleccionar Plantilla
|
||
</label>
|
||
<select class="form-control form-control-lg" id="broadcast-template" onchange="loadBroadcastTemplateDetails()">
|
||
<option value="">-- Selecciona una plantilla --</option>
|
||
</select>
|
||
<small class="form-text text-muted">
|
||
<i class="fas fa-info-circle"></i> Solo plantillas aprobadas
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Variables de plantilla -->
|
||
<div id="broadcast-template-variables" class="mb-4" style="display: none;">
|
||
<!-- Variables dinámicas -->
|
||
</div>
|
||
|
||
<!-- Vista previa de plantilla -->
|
||
<div id="broadcast-template-preview" class="mb-4" style="display: none;">
|
||
<label class="form-label fw-bold">
|
||
<i class="fas fa-eye"></i> Vista Previa
|
||
</label>
|
||
<div class="alert alert-info" id="broadcast-preview-content">
|
||
<!-- Contenido de vista previa -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="alert alert-warning d-flex align-items-center">
|
||
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
||
<div>
|
||
<strong>Importante:</strong> Este mensaje se enviará a múltiples usuarios.
|
||
<br>Verifica cuidadosamente el contenido antes de enviar.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-grid">
|
||
<button type="submit" class="btn btn-warning btn-lg">
|
||
<i class="fas fa-broadcast-tower"></i> Enviar Mensaje Masivo
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="mt-4">
|
||
<div class="card bg-light">
|
||
<div class="card-body">
|
||
<h6 class="fw-bold"><i class="fas fa-lightbulb text-warning"></i> Consejos para envío masivo:</h6>
|
||
<ul class="mb-0 small">
|
||
<li>Personaliza tu mensaje para que sea relevante</li>
|
||
<li>Evita enviar mensajes muy frecuentes</li>
|
||
<li>Respeta la ventana de 24 horas de WhatsApp</li>
|
||
<li>Usa plantillas aprobadas para nuevos contactos</li>
|
||
<li>Prueba con un grupo pequeño primero</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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&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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// 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 => ({'<':'<','>':'>','&':'&','"':'"'}[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>
|