up
This commit is contained in:
@@ -28,7 +28,7 @@ TIMEZONE=America/Bogota
|
||||
# Seguridad
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=contraseña_admin_segura
|
||||
SESSION_TIMEOUT=1800
|
||||
SESSION_TIMEOUT=31536000
|
||||
MAX_LOGIN_ATTEMPTS=3
|
||||
LOGIN_LOCKOUT_TIME=900
|
||||
|
||||
|
||||
@@ -1086,6 +1086,38 @@ body {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Unread conversation visual state: darker green and subtle background */
|
||||
.conversation-item.unread {
|
||||
background: linear-gradient(90deg, #effff5, #f6fff9);
|
||||
}
|
||||
.conversation-item.unread .conversation-avatar {
|
||||
background: linear-gradient(135deg, #0f9a6a, #0b7a57);
|
||||
box-shadow: 0 6px 18px rgba(11, 122, 87, 0.25);
|
||||
}
|
||||
|
||||
/* Notification bell indicator when there are unseen notifications */
|
||||
#notification-bell.has-notifications {
|
||||
position: relative;
|
||||
}
|
||||
#notification-bell.has-notifications::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #15803d; /* darker green */
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 3px rgba(21, 128, 61, 0.12);
|
||||
animation: pulseGreen 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseGreen {
|
||||
0% { transform: scale(1); opacity: 0.9; }
|
||||
50% { transform: scale(1.15); opacity: 0.6; }
|
||||
100% { transform: scale(1); opacity: 0.9; }
|
||||
}
|
||||
|
||||
.conversation-status-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
+18
-4
@@ -504,6 +504,8 @@ class SimpleWhatsAppManager {
|
||||
// Unread badge
|
||||
const unreadBadge = (conv.unread_count && conv.unread_count > 0) ?
|
||||
`<div class="conversation-unread">${conv.unread_count}</div>` : '';
|
||||
// Add class to highlight unread conversation (darker green)
|
||||
const unreadClass = (conv.unread_count && conv.unread_count > 0) ? ' unread' : '';
|
||||
|
||||
// Status icon
|
||||
const statusIcon = conv.last_direction === 'outgoing'
|
||||
@@ -518,7 +520,7 @@ class SimpleWhatsAppManager {
|
||||
: '';
|
||||
|
||||
html += `
|
||||
<div class="conversation-item" onclick="openChatWindow(${conv.user_id})">
|
||||
<div class="conversation-item${unreadClass}" onclick="openChatWindow(${conv.user_id})">
|
||||
<div class="conversation-avatar ${isOnline ? 'online' : ''}">
|
||||
${userInitial}
|
||||
</div>
|
||||
@@ -622,8 +624,10 @@ class SimpleWhatsAppManager {
|
||||
? '<i class="fas fa-image text-info" title="Imagen"></i> '
|
||||
: '';
|
||||
|
||||
const unreadClass = (conv.unread_count && conv.unread_count > 0) ? ' unread' : '';
|
||||
|
||||
html += `
|
||||
<div class="conversation-item" onclick="openChatWindow(${conv.user_id})">
|
||||
<div class="conversation-item${unreadClass}" onclick="openChatWindow(${conv.user_id})">
|
||||
<div class="conversation-avatar ${isOnline ? 'online' : ''}">
|
||||
${userInitial}
|
||||
</div>
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,14 @@ try {
|
||||
<div>
|
||||
<h1><i class="fas fa-robot"></i> Sistema de Gestión WhatsApp Bot</h1>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-user"></i> Conectado como <strong><?= htmlspecialchars(ADMIN_USERNAME) ?></strong>
|
||||
<i class="fas fa-user"></i> Conectado como <strong><?= htmlspecialchars(
|
||||
|
||||
// Preferir nombre completo del admin si existe
|
||||
|
||||
(
|
||||
$_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? ADMIN_USERNAME
|
||||
)
|
||||
) ?></strong>
|
||||
| <i class="fas fa-clock"></i> Sesión desde: <?= date('H:i:s', $_SESSION['login_time'] ?? time()) ?>
|
||||
</small>
|
||||
</div>
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config/config_enhanced.php';
|
||||
// Load DB connection
|
||||
require_once __DIR__ . '/../classes/Database.php';
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
/* This script was used to create the admin user 'yurley'.
|
||||
For security reasons, the original version that contained a plaintext password
|
||||
has been sanitized. To create or update admin users, use a secure script that
|
||||
accepts the password interactively or via a protected environment variable. */
|
||||
$username = 'yurley';
|
||||
$full_name = 'yurley';
|
||||
$email = 'coordinacionsig@laboratorioximenacaicedo.com';
|
||||
$sessionTimeoutSeconds = 31536000; // 1 year
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Ensure admin_users table exists (same schema as migrar_passwords_bd)
|
||||
$db->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);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config/config_enhanced.php';
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Get an existing user_id (user of conversations)
|
||||
$user = $db->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";
|
||||
}
|
||||
Reference in New Issue
Block a user