${userInitial}
@@ -1664,9 +1668,19 @@ window.debugAPI = async function (endpoint) {
};
// Función para abrir ventana de chat
-window.openChatWindow = function (userId) {
+window.openChatWindow = async function (userId) {
console.log('Abriendo chat del usuario:', userId);
-
+
+ // Marcar conversación como leída en el backend y refrescar lista localmente
+ try {
+ await fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})});
+ if (window.whatsappManager && typeof window.whatsappManager.loadConversations === 'function') {
+ window.whatsappManager.loadConversations();
+ }
+ } catch (e) {
+ console.warn('mark_conversation_read failed', e);
+ }
+
// Abrir nueva ventana de chat
const chatUrl = `chat_window.php?user_id=${userId}&debug=true`;
const windowFeatures = 'width=1000,height=700,resizable=yes,scrollbars=yes,status=yes,toolbar=no,menubar=no,location=no';
diff --git a/config/config_enhanced.php b/config/config_enhanced.php
index c7f8e7f..8771445 100644
--- a/config/config_enhanced.php
+++ b/config/config_enhanced.php
@@ -326,6 +326,9 @@ if (session_status() == PHP_SESSION_NONE) {
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']));
ini_set('session.use_strict_mode', 1);
+ // Asegurar que la cookie de sesión y GC respeten el timeout configurado (SESSION_TIMEOUT)
+ ini_set('session.cookie_lifetime', (int)SESSION_TIMEOUT);
+ ini_set('session.gc_maxlifetime', (int)SESSION_TIMEOUT);
session_start();
}
diff --git a/index.php b/index.php
index 948a6e2..9291249 100644
--- a/index.php
+++ b/index.php
@@ -72,7 +72,14 @@ try {
Sistema de Gestión WhatsApp Bot
- Conectado como = htmlspecialchars(ADMIN_USERNAME) ?>
+ Conectado como = htmlspecialchars(
+
+ // Preferir nombre completo del admin si existe
+
+ (
+ $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? ADMIN_USERNAME
+ )
+ ) ?>
| Sesión desde: = date('H:i:s', $_SESSION['login_time'] ?? time()) ?>
@@ -936,6 +943,8 @@ try {
// Global Notification Manager: polls server for unread notifications and shows toasts + native notifications
const NotificationManager = {
interval: 7000,
+ // IDs de notificaciones ya mostradas para evitar re-sonar/repetir
+ _seenIds: new Set(),
init() {
this.bell = document.getElementById('notification-bell');
this.countEl = document.getElementById('notification-count');
@@ -950,6 +959,8 @@ try {
if ("Notification" in window && Notification.permission === 'default') {
Notification.requestPermission().then(p => console.log('Notification permission:', p));
}
+ // Al abrir la campana se considera que se atendieron visualmente las notificaciones: limpiar indicador visual
+ this.bell.classList.remove('has-notifications');
});
}
@@ -971,10 +982,19 @@ try {
if (unreadCount > 0) {
this.countEl.textContent = unreadCount;
this.countEl.style.display = 'inline-block';
+ this.bell.classList.add('has-notifications');
} else {
this.countEl.style.display = 'none';
+ this.bell.classList.remove('has-notifications');
}
- json.data.forEach(n => this.showToast(n));
+
+ // Mostrar sólo las nuevas notificaciones (no repetir sonido/visual)
+ json.data.forEach(n => {
+ if (!this._seenIds.has(n.id)) {
+ this._seenIds.add(n.id);
+ this.showToast(n);
+ }
+ });
} else if (json && json.success === false) {
console.warn('Notification API error:', json.error || json);
}
@@ -1033,6 +1053,9 @@ try {
try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {}
const userId = data.user_id || notification.user_id;
if (userId) {
+ // marcar conversaciones como leídas también
+ try { await fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})}); } catch(e) { console.warn('mark_conversation_read failed', e); }
+
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
const tryOpen = () => {
@@ -1070,6 +1093,9 @@ try {
window.focus();
const userId = data.user_id || notification.user_id;
if (userId) {
+ // marcar conversación leída cuando se abre desde la notification nativa
+ try { fetch('api/mark_conversation_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({user_id: userId})}); } catch(e) { console.warn('mark_conversation_read failed', e); }
+
const convLink = document.querySelector('.nav-link[data-tab="conversations"]');
if (convLink) convLink.click();
const tryOpen = () => {
@@ -1088,6 +1114,7 @@ try {
// play sound
try {
+ // usar Web Audio API para reproducir un sonido breve
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const o = ctx.createOscillator();
const g = ctx.createGain();
diff --git a/scripts/create_admin_yurley.php b/scripts/create_admin_yurley.php
new file mode 100644
index 0000000..1c5c1d6
--- /dev/null
+++ b/scripts/create_admin_yurley.php
@@ -0,0 +1,105 @@
+execute("CREATE TABLE IF NOT EXISTS admin_users (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ username VARCHAR(50) UNIQUE NOT NULL,
+ password_hash VARCHAR(255) NOT NULL,
+ email VARCHAR(100),
+ full_name VARCHAR(100),
+ is_active TINYINT(1) DEFAULT 1,
+ last_login DATETIME NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_username (username),
+ INDEX idx_active (is_active)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
+
+ // Hash password
+ $passwordHash = password_hash($password, PASSWORD_BCRYPT);
+
+ // Check if user exists
+ $existing = $db->fetch("SELECT id FROM admin_users WHERE username = ?", [$username]);
+ if ($existing) {
+ $db->update('admin_users', [
+ 'password_hash' => $passwordHash,
+ 'email' => $email,
+ 'full_name' => $full_name,
+ 'is_active' => 1
+ ], 'id = ?', ['id' => $existing['id']]);
+ echo "Usuario '$username' actualizado (id={$existing['id']}).\n";
+ } else {
+ $id = $db->insert('admin_users', [
+ 'username' => $username,
+ 'password_hash' => $passwordHash,
+ 'email' => $email,
+ 'full_name' => $full_name,
+ 'is_active' => 1
+ ]);
+ echo "Usuario '$username' creado con id=$id.\n";
+ }
+
+ // Update system_config 'session_timeout' to 1 year (in seconds) if table exists
+ try {
+ $db->execute("CREATE TABLE IF NOT EXISTS system_config (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ config_key VARCHAR(100) UNIQUE NOT NULL,
+ config_value TEXT,
+ config_type VARCHAR(50) DEFAULT 'string',
+ description TEXT,
+ is_encrypted TINYINT(1) DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX idx_config_key (config_key)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
+
+ // Upsert session_timeout
+ $stmt = $db->getConnection()->prepare("INSERT INTO system_config (config_key, config_value, config_type, description) VALUES (?, ?, 'integer', ?) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)");
+ $stmt->execute(['session_timeout', (string)$sessionTimeoutSeconds, 'Timeout de sesión en segundos (1 año)']);
+ echo "Configuración 'session_timeout' actualizada a $sessionTimeoutSeconds segundos en DB.\n";
+ } catch (Exception $ex) {
+ echo "No se pudo actualizar system_config: " . $ex->getMessage() . "\n";
+ }
+
+ // Also update .env SESSION_TIMEOUT if writable
+ $envFile = __DIR__ . '/../.env';
+ if (is_writable($envFile)) {
+ $contents = file_get_contents($envFile);
+ if (strpos($contents, 'SESSION_TIMEOUT=') !== false) {
+ $newContents = preg_replace('/SESSION_TIMEOUT\s*=\s*\d+/', 'SESSION_TIMEOUT=' . $sessionTimeoutSeconds, $contents);
+ } else {
+ $newContents = rtrim($contents, "\n") . "\nSESSION_TIMEOUT={$sessionTimeoutSeconds}\n";
+ }
+ if (@file_put_contents($envFile, $newContents) !== false) {
+ echo ".env actualizado: SESSION_TIMEOUT={$sessionTimeoutSeconds}\n";
+ } else {
+ echo "No se pudo escribir en .env (permiso denegado).\n";
+ }
+ } else {
+ echo ".env no es escribible o no existe, se omitió actualización del archivo.\n";
+ }
+
+ echo "Hecho. Recuerda que las sesiones actuales seguirán su tiempo de expiración hasta que los usuarios vuelvan a iniciar sesión (para aplicar el nuevo timeout).\n";
+
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage() . "\n";
+ exit(1);
+}
diff --git a/scripts/test_add_notification.php b/scripts/test_add_notification.php
new file mode 100644
index 0000000..b064be2
--- /dev/null
+++ b/scripts/test_add_notification.php
@@ -0,0 +1,23 @@
+fetch('SELECT id, phone_number FROM users LIMIT 1');
+$userId = $user['id'] ?? null;
+
+$data = [
+ 'user_id' => $userId,
+ 'type' => 'message',
+ 'message' => 'Prueba: nuevo mensaje recibido',
+ 'data' => json_encode(['phone' => $user['phone_number'] ?? '']),
+ 'is_read' => 0,
+ 'created_at' => date('Y-m-d H:i:s')
+];
+
+$id = $db->insert('notifications', $data);
+if ($id) {
+ echo "Inserted notification id=$id for user_id={$userId}\n";
+} else {
+ echo "Failed to insert notification\n";
+}