Implementar tema claro/oscuro con Bootstrap 5

- Agregar atributo data-bs-theme al HTML
- Implementar botón toggle con íconos sol/luna en navegación
- Agregar estilos CSS para modo oscuro (variables, componentes, tablas)
- Implementar JavaScript para funcionalidad toggle con persistencia localStorage
- Agregar detección automática del tema del sistema
- Fix específico para columna "Contenido (Previo)" en sent_messages.php
- Mejorar Content Security Policy para archivos .map de Bootstrap
- Configuración de entorno automática para .env.pruebas

Características:
- Toggle claro/oscuro con persistencia
- Detección automática de preferencias del sistema
- Estilos personalizados para componentes en modo oscuro
- Compatibilidad con todas las páginas del sistema
This commit is contained in:
2026-01-17 16:13:19 -06:00
commit 48671dc88e
6 changed files with 1013 additions and 0 deletions

422
assets/css/style.css Executable file
View File

@@ -0,0 +1,422 @@
/* Theme Toggle Styles */
.theme-toggle-btn {
color: #f8f9fa !important; /* Light color for the icon */
border: 1px solid #f8f9fa !important; /* Light color for the border */
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
.theme-toggle-btn:hover {
background-color: #f8f9fa;
color: #212529 !important; /* Dark color for icon on hover */
}
/* General Styles */
body {
overflow-x: hidden;
}
/* Dark Mode Variables */
[data-bs-theme="dark"] {
--bs-body-bg: #1a1a1a;
--bs-body-color: #e9ecef;
--bs-border-color: #495057;
}
[data-bs-theme="dark"] #theme-toggle:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* Custom dark mode adjustments */
[data-bs-theme="dark"] .message.in {
background-color: #2d3748;
color: #e2e8f0;
}
[data-bs-theme="dark"] .message.out {
background-color: #2563eb;
color: white;
}
[data-bs-theme="dark"] .card {
background-color: #2d3748;
border-color: #4a5568;
}
[data-bs-theme="dark"] .table {
color: #e9ecef;
}
[data-bs-theme="dark"] .form-control {
background-color: #2d3748;
border-color: #4a5568;
color: #e9ecef;
}
[data-bs-theme="dark"] .form-control:focus {
background-color: #2d3748;
border-color: #2563eb;
color: #e9ecef;
}
[data-bs-theme="dark"] .navbar {
background-color: #1a1a1a !important;
}
[data-bs-theme="dark"] .dropdown-menu {
background-color: #2d3748;
border-color: #4a5568;
}
[data-bs-theme="dark"] .dropdown-item {
color: #e9ecef;
}
[data-bs-theme="dark"] .dropdown-item:hover {
background-color: #4a5568;
color: #e9ecef;
}
/* Fix for message preview in dark mode */
[data-bs-theme="dark"] .message-preview {
color: #e9ecef !important;
background-color: transparent !important;
}
/* More specific fix for table cells in dark mode */
[data-bs-theme="dark"] .table td {
color: #e9ecef !important;
border-color: #4a5568 !important;
}
[data-bs-theme="dark"] .table tbody tr {
background-color: #2d3748 !important;
}
[data-bs-theme="dark"] .table tbody tr:nth-child(odd) {
background-color: #1a202c !important;
}
/* Universal fix for all text in dark mode tables */
[data-bs-theme="dark"] .table * {
color: #e9ecef !important;
}
[data-bs-theme="dark"] .table .text-muted {
color: #a0aec0 !important;
}
[data-bs-theme="dark"] .table .badge {
color: white !important;
}
/* Fix for card container in dark mode */
[data-bs-theme="dark"] .card-body {
background-color: #2d3748 !important;
color: #e9ecef !important;
}
/* Table container fix */
[data-bs-theme="dark"] .table-responsive {
background-color: #2d3748 !important;
}
/* Specific fix for text content in dark mode */
[data-bs-theme="dark"] .text-content {
color: #212529 !important;
background-color: #f8f9fa !important;
padding: 8px !important;
border-radius: 4px !important;
display: inline-block !important;
}
[data-bs-theme="dark"] td.text-break {
color: #212529 !important;
background-color: #f8f9fa !important;
}
/* Alternative: Force dark theme for all table cells */
[data-bs-theme="dark"] .table td:nth-child(3) {
color: #212529 !important;
background-color: #f8f9fa !important;
}
[data-bs-theme="dark"] .table td:nth-child(3) div {
color: #212529 !important;
background-color: #f8f9fa !important;
}
/* FIX FOR MESSAGE PREVIEW - MOST SPECIFIC SELECTORS */
html[data-bs-theme="dark"] body .container-fluid .card .card-body .table-responsive .table tbody tr td:nth-child(3),
html[data-bs-theme="dark"] .table td:nth-child(3),
body[data-bs-theme="dark"] .table td:nth-child(3) {
background-color: #2d3748 !important;
border-color: #4a5568 !important;
color: #e9ecef !important;
}
html[data-bs-theme="dark"] body .container-fluid .card .card-body .table-responsive .table tbody tr td:nth-child(3) .message-preview,
html[data-bs-theme="dark"] .table td:nth-child(3) .message-preview {
background-color: #374151 !important;
color: #f3f4f6 !important;
padding: 8px !important;
border-radius: 4px !important;
display: inline-block !important;
border: 1px solid #4a5568 !important;
}
/* Override any conflicting styles */
html[data-bs-theme="dark"] td:nth-child(3) *,
html[data-bs-theme="dark"] td:nth-child(3) div,
html[data-bs-theme="dark"] td:nth-child(3) span {
color: #f3f4f6 !important;
}
/* ULTRA SPECIFIC - Target the exact table */
#sent-messages-table[data-bs-theme="dark"] .table td:nth-child(3),
#sent-messages-table .table td:nth-child(3)[data-bs-theme="dark"],
.card[data-bs-theme="dark"] .table td:nth-child(3) {
background: #2d3748 !important;
color: #f3f4f6 !important;
}
#sent-messages-table[data-bs-theme="dark"] .table td:nth-child(3) .message-preview,
#sent-messages-table .table td:nth-child(3)[data-bs-theme="dark"] .message-preview {
background: #374151 !important;
color: #ffffff !important;
padding: 8px !important;
border-radius: 4px !important;
border: 1px solid #4a5568 !important;
}
/* Additional dark mode fixes */
[data-bs-theme="dark"] .table-light {
background-color: #2d3748 !important;
color: #e9ecef !important;
}
[data-bs-theme="dark"] .table-hover tbody tr:hover {
background-color: #4a5568 !important;
}
[data-bs-theme="dark"] .text-muted {
color: #a0aec0 !important;
}
[data-bs-theme="dark"] .badge {
color: white !important;
}
[data-bs-theme="dark"] .badge.bg-info {
background-color: #3182ce !important;
}
/* Estilos para el chat */
#chat-history {
height: 70vh;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.message {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 1rem;
word-wrap: break-word;
position: relative;
line-height: 1.4;
}
/* Mensajes entrantes */
.message.in {
background-color: #f0f0f0;
color: #333;
align-self: flex-start;
border-bottom-left-radius: 0.25rem;
}
/* Mensajes salientes */
.message.out {
background-color: #007bff;
color: white;
align-self: flex-end;
border-bottom-right-radius: 0.25rem;
}
/* Estilo para el nombre del remitente en grupos */
.message-sender {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
/* Estilo para el texto del mensaje */
.message-text {
word-wrap: break-word;
}
/* Estilo para el contenedor del formulario de mensajes */
#message-form-container {
padding: 1rem;
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
}
/* Estilos para la lista de usuarios */
.user-list {
max-height: 80vh;
overflow-y: auto;
}
.user-list .list-group-item {
cursor: pointer;
transition: background-color 0.2s;
}
.user-list .list-group-item:hover {
background-color: #f8f9fa;
}
.user-list .list-group-item.active {
background-color: #007bff;
border-color: #007bff;
}
#wrapper {
display: flex;
transition: all 0.3s ease;
}
#sidebar-wrapper {
display: none;
}
#page-content-wrapper {
min-width: 100vw;
flex-grow: 1;
}
/* Navbar styles */
.navbar-brand {
font-weight: bold;
font-size: 1.2rem;
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
transition: color 0.2s;
}
.navbar-dark .navbar-nav .nav-link:hover {
color: white;
}
.navbar-dark .navbar-nav .nav-link.active {
color: white;
}
.dropdown-menu {
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-item {
transition: background-color 0.2s;
}
.dropdown-item:hover {.theme-toggle-btn
background-color: #f8f9fa;
}
.dropdown-item.active, .dropdown-item:active {
background-color: #0d6efd;
color: white;
}
/* Responsive navbar */
@media (max-width: 991px) {
.navbar-collapse {
margin-top: 1rem;
}
.d-flex.align-items-center.gap-2 {
flex-direction: column;
align-items: flex-start !important;
margin-top: 1rem;
}
}
#wrapper.toggled #page-content-wrapper {
min-width: calc(100vw - 250px);
}
.sidebar-heading {
padding: 0.875rem 1.25rem;
font-size: 1.2rem;
}
@media (min-width: 768px) {
#sidebar-wrapper {
margin-left: 0;
position: relative; /* Changed for desktop */
z-index: 1; /* Changed for desktop */
}
#page-content-wrapper {
min-width: 0;
width: 100%;
}
#wrapper.toggled #sidebar-wrapper {
margin-left: -250px;
}
}
/* Overlay for mobile when sidebar is open */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
opacity: 0; /* Initial opacity */
transition: opacity 0.3s ease-in-out; /* Smooth transition */
}
.overlay.show {
display: block;
opacity: 1; /* Full opacity when shown */
}
/* Estilos para el logo en el sidebar */
#sidebar-logo {
width: 40px; /* Tamaño fijo para el ancho */
height: auto; /* Mantiene la proporción de aspecto */
vertical-align: middle; /* Alinea verticalmente con el texto */
margin-left: 8px; /* Espaciado a la izquierda del logo */
}
.sidebar-heading {
display: flex;
align-items: center; /* Centra verticalmente el texto y el logo */
padding: 0.875rem 1.25rem;
font-size: 1.2rem;
white-space: nowrap; /* Evita que el texto se rompa en varias líneas */
}

46
assets/js/main.js Executable file
View File

@@ -0,0 +1,46 @@
$(document).ready(function(){
$("#menu-toggle").click(function(e) {
e.preventDefault();
$("#wrapper").toggleClass("toggled");
});
$(".overlay").click(function() {
$("#wrapper").removeClass("toggled");
});
// Theme Toggle Functionality
const themeToggle = $('#theme-toggle');
const themeIcon = $('#theme-icon');
const html = $('html');
// Load saved theme or default to light
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
themeToggle.on('click', function() {
const currentTheme = html.attr('data-bs-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
});
function setTheme(theme) {
html.attr('data-bs-theme', theme);
// Update icon
if (theme === 'dark') {
themeIcon.removeClass('bi-sun-fill').addClass('bi-moon-fill');
themeToggle.attr('title', 'Cambiar a tema claro');
} else {
themeIcon.removeClass('bi-moon-fill').addClass('bi-sun-fill');
themeToggle.attr('title', 'Cambiar a tema oscuro');
}
}
// Check system preference on first load
if (!localStorage.getItem('theme')) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDark ? 'dark' : 'light');
localStorage.setItem('theme', prefersDark ? 'dark' : 'light');
}
});

75
common/helpers/url_helper.php Executable file
View File

@@ -0,0 +1,75 @@
<?php
// Incluir el archivo de configuración
require_once __DIR__ . '/../../config/config.php';
/**
* Establece las cabeceras de seguridad, incluida la Content-Security-Policy.
* Debe llamarse antes de cualquier salida HTML.
*/
function set_security_headers() {
// CSP para permitir recursos solo del propio dominio y CDNs de confianza
$csp = "default-src 'self'; "
. "script-src 'self' https://cdn.jsdelivr.net https://code.jquery.com 'unsafe-inline'; " // unsafe-inline para scripts de bootstrap si es necesario
. "style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; " // unsafe-inline para estilos de bootstrap
. "img-src 'self' data: " . BOT_BASE_URL . "; " // permitir imágenes del propio dominio, data URIs y el dominio externo
. "font-src 'self' https://cdn.jsdelivr.net; "
. "connect-src 'self' https://cdn.jsdelivr.net;"; // Para archivos .map de Bootstrap
header("Content-Security-Policy: " . $csp);
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");
}
/**
* Genera una URL absoluta para el sitio
*
* @param string $path Ruta relativa
* @return string URL completa
*/
function site_url($path = '') {
return rtrim(BOT_BASE_URL, '/') . '/' . ltrim($path, '/');
}
/**
* Genera una URL para los assets (CSS, JS, imágenes, etc.)
*
* @param string $path Ruta relativa al archivo de asset
* @return string URL completa al asset
*/
function asset($path) {
return site_url('assets/' . ltrim($path, '/'));
}
/**
* Redirige al usuario a una URL específica
*
* @param string $url URL a la que redirigir
* @param int $statusCode Código de estado HTTP (por defecto 302)
*/
function redirect($url, $statusCode = 302) {
header('Location: ' . $url, true, $statusCode);
exit();
}
/**
* Obtiene la URL actual completa
*
* @param string $path Ruta relativa
* @return string URL completa
*/
function current_url() {
$protocol = "http://";
return $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
/**
* Verifica si la URL actual coincide con el patrón dado
*
* @param string $pattern Patrón a verificar
* @return bool Verdadero si coincide, falso en caso contrario
*/
function is_current_url($pattern) {
$current = current_url();
return (strpos($current, $pattern) !== false);
}

148
config/config.php Executable file
View File

@@ -0,0 +1,148 @@
<?php
// config/config.php
// Cargar variables de entorno
require_once __DIR__ . '/../vendor/autoload.php';
// Verificar si estamos en un contenedor Docker
$is_docker = (getenv('DOCKER_CONTAINER') === '1') || ($_SERVER['DOCKER_CONTAINER'] ?? null) === '1';
if ($is_docker) {
// Docker: cargar archivo .env creado por el entrypoint
$dotenv = null;
if (file_exists(dirname(__DIR__) . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
try {
$dotenv->load();
} catch (Exception $e) {
die('Error al cargar el archivo de entorno en Docker: ' . $e->getMessage());
}
}
} else {
// Entorno local: cargar archivo .env según APP_ENVIRONMENT
$env = getenv('APP_ENVIRONMENT') ?: ($_SERVER['APP_ENVIRONMENT'] ?? 'pruebas');
$envFile = '.env';
if ($env) {
$envFile = '.env.' . $env;
}
$dotenv = null;
if (file_exists(dirname(__DIR__) . '/' . $envFile)) {
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__), $envFile);
} elseif (file_exists(dirname(__DIR__) . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
}
if ($dotenv) {
try {
$dotenv->load();
} catch (Exception $e) {
die('Error al cargar el archivo de entorno: ' . $e->getMessage());
}
$dotenv->required([
'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS',
'JWT_SECRET', 'APP_URL'
]);
}
}
// Environment Configuration
define('ENVIRONMENT', $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'production');
// Detectar si se ejecuta desde la línea de comandos
$is_cli = (php_sapi_name() === 'cli' || defined('STDIN'));
// Helper function to get env vars
function getEnvVar($name, $default = null) {
return $_ENV[$name] ?? $_SERVER[$name] ?? getenv($name) ?? $default;
}
// Configurar la URL base y el protocolo
if ($is_cli) {
define('BOT_BASE_URL', getEnvVar('APP_URL'));
$protocol = 'http';
} else {
$protocol = 'http';
if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
(!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ||
(!empty($_SERVER['HTTP_CF_VISITOR']) && strpos($_SERVER['HTTP_CF_VISITOR'], 'https') !== false)) {
$protocol = 'https';
$_SERVER['HTTPS'] = 'on';
$_SERVER['SERVER_PORT'] = 443;
}
$app_url = getEnvVar('APP_URL');
if ($app_url) {
define('BOT_BASE_URL', $app_url);
} else {
define('BOT_BASE_URL', $protocol . '://' . $_SERVER['HTTP_HOST']);
}
$_SERVER['REQUEST_SCHEME'] = $protocol;
}
define('BASE_PATH', dirname(__DIR__));
// Database Configuration
define('DB_HOST', getEnvVar('DB_HOST'));
define('DB_USER', getEnvVar('DB_USER'));
define('DB_PASS', getEnvVar('DB_PASS'));
define('DB_NAME', getEnvVar('DB_NAME'));
define('DB_DIALECT', getEnvVar('DB_DIALECT'));
define('DB_PORT', getEnvVar('DB_PORT'));
// Session Configuration
define('SESSION_SECRET', getEnvVar('JWT_SECRET'));
// Discord API Configuration
define('DISCORD_GUILD_ID', getEnvVar('DISCORD_GUILD_ID'));
define('DISCORD_CLIENT_ID', getEnvVar('DISCORD_CLIENT_ID'));
define('DISCORD_CLIENT_SECRET', getEnvVar('DISCORD_CLIENT_SECRET'));
define('DISCORD_BOT_TOKEN', getEnvVar('DISCORD_BOT_TOKEN'));
// Telegram API Configuration
define('TELEGRAM_BOT_TOKEN', getEnvVar('TELEGRAM_BOT_TOKEN'));
define('TELEGRAM_WEBHOOK_TOKEN', getEnvVar('TELEGRAM_WEBHOOK_TOKEN'));
// LibreTranslate Configuration
define('LIBRETRANSLATE_URL', getEnvVar('LIBRETRANSLATE_URL'));
// N8N Configuration
define('N8N_URL', getEnvVar('N8N_URL'));
define('N8N_TOKEN', getEnvVar('N8N_TOKEN'));
define('N8N_PROCESS_QUEUE_WEBHOOK_URL', getEnvVar('N8N_PROCESS_QUEUE_WEBHOOK_URL'));
define('N8N_IA_WEBHOOK_URL', getEnvVar('N8N_IA_WEBHOOK_URL'));
define('N8N_IA_WEBHOOK_URL_DISCORD', getEnvVar('N8N_IA_WEBHOOK_URL_DISCORD'));
define('INTERNAL_API_KEY', getEnvVar('INTERNAL_API_KEY'));
// Error Reporting
switch (ENVIRONMENT) {
case 'development':
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
ini_set('error_log', dirname(__DIR__) . '/logs/php_errors.log');
break;
case 'production':
error_reporting(E_ALL & ~E_DEPRECATED);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', dirname(__DIR__) . '/logs/php_errors.log');
break;
default:
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
ini_set('error_log', dirname(__DIR__) . '/logs/php_errors.log');
break;
}
// Helper function to get full URL
function url($path = '') {
$path = ltrim($path, '/');
return BOT_BASE_URL . '/' . $path;
}
// Helper function to get full asset URL
function asset_url($path = '') {
$path = ltrim($path, '/');
return BOT_BASE_URL . '/assets/' . $path;
}

171
sent_messages.php Executable file
View File

@@ -0,0 +1,171 @@
<?php
require_once __DIR__ . '/includes/session_check.php';
require_once __DIR__ . '/includes/db.php';
try {
$query = "
SELECT
sm.id as sent_message_id,
sm.platform_message_id,
s.sent_at,
m.content,
u.username as creator_username,
r.name as recipient_name,
r.type as recipient_type,
r.platform,
r.platform_id
FROM sent_messages sm
JOIN schedules s ON sm.schedule_id = s.id
JOIN messages m ON s.message_id = m.id
JOIN users u ON m.user_id = u.id
JOIN recipients r ON sm.recipient_id = r.id
ORDER BY s.sent_at DESC
";
$stmt = $pdo->prepare($query);
$stmt->execute();
$sentMessages = $stmt->fetchAll();
} catch (PDOException $e) {
error_log("Error al consultar mensajes enviados: " . $e->getMessage());
$sentMessages = [];
$db_error = "Error de base de datos: " . $e->getMessage();
}
$pageTitle = 'Mensajes Enviados';
require_once __DIR__ . '/templates/header.php';
?>
<div class="container-fluid">
<h1 class="mt-4" data-translate="true">Mensajes Enviados</h1>
<?php if (isset($db_error)): ?>
<div class="alert alert-danger"><?= htmlspecialchars($db_error) ?></div>
<?php endif; ?>
<?php
// Obtener la plataforma del mensaje que se está eliminando (si está presente en la URL)
$platform = $_GET['platform'] ?? 'Discord'; // Por defecto a Discord para mantener compatibilidad
$platform = ucfirst(strtolower($platform)); // Asegurar que la primera letra sea mayúscula
?>
<?php if (isset($_GET['success']) && $_GET['success'] === 'deleted'): ?>
<div class="alert alert-success"><span data-translate="true">Mensaje eliminado de</span> <?= htmlspecialchars($platform) ?> <span data-translate="true">con éxito</span>.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="alert alert-danger">
<?php
$errorMessage = '<span data-translate="true">Ocurrió un error desconocido.</span>';
if ($_GET['error'] === 'delete_failed') {
$errorMessage = "<span data-translate='true'>No se pudo eliminar el mensaje de</span> {$platform}.";
if (isset($_GET['message'])) {
$errorMessage .= ' ' . htmlspecialchars($_GET['message']);
}
}
echo htmlspecialchars($errorMessage);
?>
</div>
<?php endif; ?>
<div class="card shadow-sm" id="sent-messages-table">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th data-translate="true">Plataforma</th>
<th data-translate="true">Destinatario</th>
<th data-translate="true">Contenido (Previo)</th>
<th data-translate="true">Fecha de Envío</th>
<th data-translate="true">Creado por</th>
<th class="text-center" data-translate="true">Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($sentMessages)): ?>
<tr>
<td colspan="6" class="text-center text-muted" data-translate="true">No se han enviado mensajes todavía.</td>
</tr>
<?php else: ?>
<?php foreach ($sentMessages as $msg): ?>
<tr>
<td>
<span class="badge <?= $msg['platform'] === 'discord' ? 'bg-primary' : 'bg-info text-dark' ?>">
<i class="bi bi-<?= $msg['platform'] === 'discord' ? 'discord' : 'telegram' ?>"></i>
<?= htmlspecialchars(ucfirst($msg['platform'])) ?>
</span>
</td>
<td><?= htmlspecialchars($msg['recipient_name']) . ' <span class="text-muted">(' . $msg['recipient_type'] . ')</span>' ?></td>
<td class="text-break">
<div class="message-preview text-content">
<?= substr(strip_tags($msg['content']), 0, 100) ?>...
</div>
</td>
<td><?= date('d/m/Y H:i', strtotime($msg['sent_at'])) ?></td>
<td><?= htmlspecialchars($msg['creator_username']) ?></td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<form action="create_message.php" method="POST" class="d-inline">
<input type="hidden" name="action" value="reuse">
<input type="hidden" name="messageContent" value="<?= htmlspecialchars($msg['content'], ENT_QUOTES, 'UTF-8') ?>">
<button type="submit" class="btn btn-sm btn-secondary" title="Reutilizar este mensaje" data-translate-title="true">
<i class="bi bi-recycle"></i>
</button>
</form>
<?php if (!empty($msg['platform_message_id'])): ?>
<?php if ($msg['platform'] === 'discord'): ?>
<form action="includes/discord_actions.php" method="POST" onsubmit="return confirm(this.querySelector('[data-translate-confirm]').getAttribute('data-translate-confirm'));" class="d-inline">
<input type="hidden" name="action" value="delete_message">
<input type="hidden" name="sent_message_id" value="<?= $msg['sent_message_id'] ?>">
<input type="hidden" name="platform_message_id" value="<?= htmlspecialchars($msg['platform_message_id'], ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="channel_id" value="<?= htmlspecialchars($msg['platform_id'], ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="confirm_message" value="¿Estás seguro de que quieres ELIMINAR este mensaje de Discord?" data-translate-confirm="¿Estás seguro de que quieres ELIMINAR este mensaje de Discord?">
<button type="submit" class="btn btn-sm btn-danger" title="Eliminar de Discord" data-translate-title="true">
<i class="bi bi-trash-fill"></i>
</button>
</form>
<?php elseif ($msg['platform'] === 'telegram'): ?>
<form action="includes/telegram_actions.php" method="POST" onsubmit="return confirm(this.querySelector('[data-translate-confirm]').getAttribute('data-translate-confirm'));" class="d-inline">
<input type="hidden" name="action" value="delete_message">
<input type="hidden" name="sent_message_id" value="<?= $msg['sent_message_id'] ?>">
<input type="hidden" name="platform_message_id" value="<?= htmlspecialchars($msg['platform_message_id'], ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="chat_id" value="<?= htmlspecialchars($msg['platform_id'], ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="confirm_message" value="¿Estás seguro de que quieres ELIMINAR este mensaje de Telegram?" data-translate-confirm="¿Estás seguro de que quieres ELIMINAR este mensaje de Telegram?">
<button type="submit" class="btn btn-sm btn-danger" title="Eliminar de Telegram" data-translate-title="true">
<i class="bi bi-trash-fill"></i>
</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.message-preview {
max-height: 80px;
overflow-y: auto;
padding: 8px;
background-color: var(--bs-light);
border: 1px solid var(--bs-border-color);
border-radius: 5px;
font-size: 0.9em;
min-width: 200px;
}
[data-bs-theme="dark"] .message-preview {
background-color: var(--bs-dark-bg-subtle);
border-color: var(--bs-border-color-translucent);
color: var(--bs-body-color);
}
</style>
<?php require_once __DIR__ . '/templates/footer.php'; ?>

151
templates/header.php Executable file
View File

@@ -0,0 +1,151 @@
<?php
// Incluir los helpers necesarios
require_once __DIR__ . '/../includes/url_helper.php';
require_once __DIR__ . '/../includes/translation_helper.php';
// Inicializar el idioma de la sesión si no está definido
if (!isset($_SESSION['language'])) {
$_SESSION['language'] = 'es'; // Idioma por defecto
}
?>
<!DOCTYPE html>
<html lang="es" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bot Discord</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<!-- Estilos personalizados -->
<link rel="stylesheet" href="<?php echo asset('css/style.css'); ?>" type="text/css">
<!-- Contenido extra del head (opcional) -->
<?php if (isset($extraHead)) echo $extraHead; ?>
</head>
<body>
<!-- Top Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom">
<div class="container-fluid">
<!-- Logo y nombre -->
<a class="navbar-brand d-flex align-items-center" href="<?php echo site_url('index.php'); ?>">
<img src="<?php echo asset('images/logo.png'); ?>" alt="Logo" style="height: 30px; margin-right: 10px;">
<strong>Bot Discord</strong>
</a>
<!-- Toggle button para móvil -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMenu" aria-controls="navbarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Menu items -->
<div class="collapse navbar-collapse" id="navbarMenu">
<ul class="navbar-nav me-auto">
<!-- Inicio -->
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('index.php'); ?>" data-translate="true">
<i class="bi bi-house-door-fill me-1"></i>Inicio
</a>
</li>
<!-- Mensajes -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navMensajes" role="button" data-bs-toggle="dropdown" aria-expanded="false" data-translate="true">
<i class="bi bi-chat-dots-fill me-1"></i>Mensajes
</a>
<ul class="dropdown-menu" aria-labelledby="navMensajes">
<li><a class="dropdown-item" href="<?php echo site_url('create_message.php'); ?>" data-translate="true"><i class="bi bi-plus-square-fill me-2"></i>Crear Mensaje</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('scheduled_messages.php'); ?>" data-translate="true"><i class="bi bi-clock-fill me-2"></i>Programados</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('recurrentes.php'); ?>" data-translate="true"><i class="bi bi-arrow-repeat me-2"></i>Plantillas</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('sent_messages.php'); ?>" data-translate="true"><i class="bi bi-send-fill me-2"></i>Enviados</a></li>
</ul>
</li>
<!-- Recursos -->
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('gallery.php'); ?>" data-translate="true">
<i class="bi bi-images me-1"></i>Galería
</a>
</li>
<!-- Admin (solo si es admin) -->
<?php if ($_SESSION['role'] === 'admin'): ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navAdmin" role="button" data-bs-toggle="dropdown" aria-expanded="false" data-translate="true">
<i class="bi bi-gear-fill me-1"></i>Admin
</a>
<ul class="dropdown-menu" aria-labelledby="navAdmin">
<li><h6 class="dropdown-header">Gestión</h6></li>
<li><a class="dropdown-item" href="<?php echo site_url('admin/users.php'); ?>" data-translate="true"><i class="bi bi-people-fill me-2"></i>Usuarios</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('admin/recipients.php'); ?>" data-translate="true"><i class="bi bi-person-rolodex me-2"></i>Destinatarios</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Configuración</h6></li>
<li><a class="dropdown-item" href="<?php echo site_url('admin/languages.php'); ?>" data-translate="true"><i class="bi bi-translate me-2"></i>Idiomas</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('admin/comandos.php'); ?>" data-translate="true"><i class="bi bi-terminal-fill me-2"></i>Comandos</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Bots</h6></li>
<li><a class="dropdown-item" href="<?php echo site_url('telegram/admin/telegram_welcome.php'); ?>" data-translate="true"><i class="bi bi-telegram me-2"></i>Telegram Config</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('telegram/admin/telegram_bot_interactions.php'); ?>" data-translate="true"><i class="bi bi-robot me-2"></i>Interacciones Bot</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('telegram/admin/chat_telegram.php'); ?>" data-translate="true"><i class="bi bi-chat-dots-fill me-2"></i>Chat Telegram</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Monitoreo</h6></li>
<li><a class="dropdown-item" href="<?php echo site_url('admin/activity.php'); ?>" data-translate="true"><i class="bi bi-clipboard-data-fill me-2"></i>Actividad</a></li>
<li><a class="dropdown-item" href="<?php echo site_url('admin/test_discord_connection.php'); ?>" data-translate="true"><i class="bi bi-bug-fill me-2"></i>Test</a></li>
</ul>
</li>
<?php endif; ?>
<!-- User menu -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navUser" role="button" data-bs-toggle="dropdown" aria-expanded="false" style="cursor: pointer;">
<i class="bi bi-person-circle me-1"></i><?php echo htmlspecialchars($_SESSION['username']); ?>
</a>
<ul class="dropdown-menu" aria-labelledby="navUser">
<li><a class="dropdown-item" href="<?php echo site_url('profile.php'); ?>" data-translate="true"><i class="bi bi-person-circle me-2"></i>Perfil</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="<?php echo site_url('logout.php'); ?>" data-translate="true"><i class="bi bi-box-arrow-right me-2"></i>Cerrar Sesión</a></li>
</ul>
</li>
</ul>
<!-- Right side: Theme toggle, Language selector, DB info -->
<div class="d-flex align-items-center gap-2 flex-wrap ms-auto">
<!-- Theme Toggle -->
<button class="btn btn-sm theme-toggle-btn" id="theme-toggle" title="Cambiar tema" aria-label="Cambiar tema claro/oscuro">
<i class="bi bi-sun-fill" id="theme-icon"></i>
</button>
<select class="form-select form-select-sm w-auto" id="language-selector" style="max-width: 120px;">
<!-- Options will be populated by translate_frontend.js -->
</select>
<?php if ($_SESSION['role'] === 'admin'): ?>
<div class="card border-secondary bg-dark text-light" style="width: 150px; box-shadow: 0 2px 6px rgba(0,0,0,0.3); margin: 0; flex-shrink: 0;">
<div class="card-body p-1 text-center" style="font-size: 0.8rem;">
<div class="mb-1">
<i class="bi bi-database text-info" style="font-size: 14px;"></i>
</div>
<div class="mb-1">
<small class="text-muted d-block" style="font-size: 8px;">BASE DE DATOS</small>
<strong class="d-block text-light" style="font-size: 11px;"><?php echo htmlspecialchars(DB_NAME); ?></strong>
</div>
<hr class="my-1" style="margin: 0.3rem 0;">
<div>
<small class="text-muted d-block" style="font-size: 8px;">SERVIDOR</small>
<code class="text-info" style="font-size: 10px;"><?php echo htmlspecialchars(DB_HOST); ?>:<?php echo htmlspecialchars(DB_PORT); ?></code>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<div id="page-content-wrapper">
<main class="container-fluid p-4">
<!-- Conflicting language script removed -->