Primer commit del sistema separado falta mejorar mucho

This commit is contained in:
nickpons666
2025-12-30 01:18:46 -06:00
commit 1679c73e52
2384 changed files with 472342 additions and 0 deletions

304
discord/views/commands/list.php Executable file
View File

@@ -0,0 +1,304 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
// Verificar permiso para ver la página de comandos
if (!hasPermission('view_commands', 'discord')) {
die('No tienes permiso para ver los comandos de Discord.');
}
$db = getDB();
// Obtener comandos con información de la plantilla asociada
$stmt = $db->query("
SELECT c.id, p.nombre, c.comando, c.fecha_creacion, c.descripcion
FROM comandos_discord c
LEFT JOIN plantillas_discord p ON c.plantilla_id = p.id
ORDER BY c.comando ASC
");
$comandos = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Comandos Discord - Sistema de Bots</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.table-responsive {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
font-weight: 600;
color: #555;
background: #f8f9fa;
}
.command-tag {
background: #2b2d31;
color: #dbdee1;
padding: 4px 8px;
border-radius: 4px;
font-family: monospace;
font-weight: 600;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.btn-primary:hover {
background: var(--discord-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
padding: 5px 10px;
font-size: 12px;
}
.btn-edit {
background: #ffc107;
color: #212529;
padding: 5px 10px;
font-size: 12px;
margin-right: 5px;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
background: white;
margin: 10% auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 600px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
.form-control {
width: 100%;
padding: 10px;
border: 2px solid #eee;
border-radius: 8px;
}
.close {
float: right;
font-size: 24px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-terminal"></i> Comandos Discord</h1>
<div style="display: flex; gap: 10px;">
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
<?php if (hasPermission('manage_templates', 'discord')): ?>
<a href="/discord/views/templates/create.php" class="btn btn-primary">
<i class="fas fa-plus"></i> Nuevo Comando (Plantilla)
</a>
<?php endif; ?>
</div>
</div>
<div class="container">
<div class="card">
<?php if (empty($comandos)): ?>
<div style="text-align: center; padding: 40px; color: #666;">
<i class="fas fa-terminal" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
<h3>No hay comandos configurados</h3>
<p>Los comandos se definen al crear o editar una plantilla.</p>
<?php if (hasPermission('manage_templates', 'discord')): ?>
<a href="/discord/views/templates/create.php" class="btn btn-primary" style="margin-top: 10px;">
Ir a Plantillas
</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="table-responsive">
<table>
<thead>
<tr>
<th>Comando</th>
<th>Plantilla Asociada</th>
<th>Fecha Creación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($comandos as $cmd): ?>
<tr>
<td>
<span class="command-tag"><?php echo htmlspecialchars($cmd['comando']); ?></span>
</td>
<td>
<i class="fas fa-file-alt"></i> <?php echo htmlspecialchars($cmd['nombre']); ?>
</td>
<td>
<?php echo date('d/m/Y', strtotime($cmd['fecha_creacion'])); ?>
</td>
<td>
<?php if (hasPermission('manage_templates', 'discord')): ?>
<a href="/discord/views/templates/edit.php?id=<?php echo $cmd['id']; ?>" class="btn btn-edit">
<i class="fas fa-edit"></i> Editar
</a>
<button onclick="deleteCommand(<?php echo $cmd['id']; ?>, '<?php echo htmlspecialchars($cmd['comando'], ENT_QUOTES); ?>')" class="btn btn-danger">
<i class="fas fa-trash"></i> Eliminar
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Scripts eliminados ya que no se necesita modal local -->
<script>
async function deleteCommand(templateId, commandName) {
if (!confirm(`¿Estás seguro de eliminar el comando "${commandName}"? Esto eliminará también la plantilla asociada.`)) {
return;
}
try {
const response = await fetch('/discord/api/templates/delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: templateId })
});
const result = await response.json();
if (result.success) {
alert('Comando y plantilla eliminados correctamente.');
location.reload(); // Recargar la página para actualizar la lista
} else {
alert('Error al eliminar: ' + (result.error || 'Error desconocido.'));
}
} catch (error) {
console.error('Error al enviar la solicitud de eliminación:', error);
alert('Error de conexión al intentar eliminar el comando.');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
[04-Dec-2025 16:44:49 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'comando' in 'field list' in /var/www/html/bot/discord/views/commands/list.php:23
Stack trace:
#0 /var/www/html/bot/discord/views/commands/list.php(23): PDO->query()
#1 {main}
thrown in /var/www/html/bot/discord/views/commands/list.php on line 23

113
discord/views/features.php Executable file
View File

@@ -0,0 +1,113 @@
<?php
require_once __DIR__ . '/../../shared/bootstrap.php';
// El bootstrap.php ya maneja la autenticación
if (!hasPermission('view_logs', 'discord')) {
die('No tienes permiso para ver esta página.');
}
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Funciones del Bot de Discord</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white; border-radius: 15px; padding: 20px 30px;
margin-bottom: 30px; display: flex; justify-content: space-between;
align-items: center; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 { color: var(--discord-color); font-size: 24px; }
.btn-back {
background: #6c757d; color: white; padding: 10px 20px;
border-radius: 8px; text-decoration: none; transition: transform 0.2s;
}
.btn-back:hover { transform: translateY(-2px); }
.container { max-width: 1000px; margin: 0 auto; }
.card {
background: white; border-radius: 15px; padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); margin-bottom: 20px;
}
.feature {
border-bottom: 1px solid #eee;
padding: 20px 0;
}
.feature:last-child { border-bottom: none; }
.feature-title { font-size: 18px; font-weight: 700; color: #333; margin-bottom: 8px; }
.feature-event { font-size: 14px; color: var(--discord-color); font-family: monospace; margin-bottom: 8px; }
.feature-description { font-size: 15px; color: #666; line-height: 1.6; }
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-robot"></i> Funciones del Bot de Discord</h1>
<a href="/discord/dashboard_discord.php" class="btn-back">← Volver al Dashboard</a>
</div>
<div class="container">
<div class="card">
<div class="feature">
<div class="feature-title">Mensajes de Bienvenida a Nuevos Miembros</div>
<div class="feature-event">Evento: Un nuevo usuario se une al servidor</div>
<p class="feature-description">
Cuando un nuevo miembro se une, el bot le da la bienvenida en un canal específico. Este mensaje es totalmente configurable desde el panel de administración, permitiendo cambiar el texto, añadir una imagen y habilitar botones para que el usuario seleccione su idioma preferido.
</p>
</div>
<div class="feature">
<div class="feature-title">Sistema de Plantillas y Comandos</div>
<div class="feature-event">Evento: Un usuario escribe un comando como #ayuda o /comandos</div>
<p class="feature-description">
El bot puede responder a comandos que empiezan con <code>#</code>. Cada comando está asociado a una plantilla de texto predefinida.
<br>- <strong>Comandos de Plantilla:</strong> Al usar un comando como <code>#reglas</code>, el bot envía el contenido de esa plantilla al canal.
<br>- <strong>Traducción de Plantillas:</strong> Junto con la plantilla, el bot añade botones para que los usuarios puedan traducirla instantáneamente a los idiomas activos.
<br>- <strong>Listar Comandos:</strong> El comando <code>/comandos</code> muestra una lista de todos los comandos de plantilla disponibles.
</p>
</div>
<div class="feature">
<div class="feature-title">Traducción Automática de Mensajes</div>
<div class="feature-event">Evento: Al recibir un mensaje que no es un comando</div>
<p class="feature-description">
Para romper las barreras del idioma, el bot añade un botón <strong>"Traducir / Translate"</strong> debajo de los mensajes que no son comandos. Al hacer clic, el bot traduce el mensaje original al idioma que el usuario haya configurado y se lo envía como un mensaje privado (efímero) que solo él puede ver.
</p>
</div>
<div class="feature">
<div class="feature-title">Gestión de Preferencias de Idioma</div>
<div class="feature-event">Evento: Clic en un botón de selección de idioma</div>
<p class="feature-description">
El bot permite a los usuarios establecer su idioma preferido.
<br>- <strong>Botones de Bienvenida:</strong> Al hacer clic en los botones de idioma del mensaje de bienvenida, el bot guarda la preferencia del usuario.
<br>- <strong>Traducciones Personalizadas:</strong> Esta preferencia se usa para saber a qué idioma traducir cuando el usuario utiliza una función de traducción.
</p>
</div>
<div class="feature">
<div class="feature-title">Registro de Usuarios para Mejor Interacción</div>
<div class="feature-event">Evento: Al unirse un miembro o interactuar por primera vez</div>
<p class="feature-description">
Para personalizar la experiencia, el bot guarda información básica de los usuarios.
<br>- <strong>Al unirse al servidor:</strong> Si está activado en la configuración de bienvenida, el bot registra al nuevo miembro.
<br>- <strong>Al seleccionar un idioma:</strong> El bot registra o actualiza al usuario que interactúa con los botones de idioma para guardar su preferencia.
</p>
</div>
</div>
</div>
</body>
</html>

322
discord/views/logs/list.php Executable file
View File

@@ -0,0 +1,322 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
$db = getDB();
// Paginación
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$perPage = 20;
$offset = ($page - 1) * $perPage;
// Filtros
$nivel = isset($_GET['nivel']) ? $_GET['nivel'] : '';
$origen = isset($_GET['origen']) ? $_GET['origen'] : '';
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
// Construir Query
$where = ["1=1"];
$params = [];
if ($nivel) {
$where[] = "nivel = ?";
$params[] = $nivel;
}
if ($origen) {
$where[] = "origen = ?";
$params[] = $origen;
}
if ($search) {
$where[] = "descripcion LIKE ?";
$params[] = "%$search%";
}
$whereClause = implode(" AND ", $where);
// Total para paginación
$stmt = $db->prepare("SELECT COUNT(*) FROM logs_discord WHERE $whereClause");
$stmt->execute($params);
$totalLogs = $stmt->fetchColumn();
$totalPages = ceil($totalLogs / $perPage);
// Obtener logs
$stmt = $db->prepare("
SELECT l.*, u.username
FROM logs_discord l
LEFT JOIN usuarios u ON l.usuario_id = u.id
WHERE $whereClause
ORDER BY l.fecha DESC
LIMIT $perPage OFFSET $offset
");
$stmt->execute($params);
$logs = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logs Discord - Sistema de Bots</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
--bg-color: #f0f2f5;
--text-color: #333;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
--info: #17a2b8;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 { color: var(--discord-color); font-size: 24px; }
.container { max-width: 1200px; margin: 0 auto; }
.card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.form-control {
padding: 10px;
border: 2px solid #eee;
border-radius: 8px;
min-width: 150px;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
border: none;
cursor: pointer;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary { background: var(--discord-color); color: white; }
.btn-secondary { background: #6c757d; color: white; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; color: #555; font-weight: 600; }
.badge {
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: 600;
color: white;
}
.badge-info { background: var(--info); }
.badge-warning { background: var(--warning); color: #333; }
.badge-error { background: var(--danger); }
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.pagination a {
padding: 8px 12px;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
text-decoration: none;
color: var(--discord-color);
}
.pagination a.active {
background: var(--discord-color);
color: white;
border-color: var(--discord-color);
}
/* Modal JSON */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.modal-content { background: white; margin: 10% auto; padding: 25px; width: 80%; max-width: 800px; border-radius: 15px; position: relative; max-height: 80vh; overflow-y: auto; }
.close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; color: #aaa; }
pre {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
font-family: monospace;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-list-alt"></i> Logs del Sistema</h1>
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
<div class="container">
<div class="card">
<form class="filters" method="GET">
<input type="text" name="search" class="form-control" placeholder="Buscar en descripción..." value="<?php echo htmlspecialchars($search); ?>">
<select name="nivel" class="form-control">
<option value="">-- Todos los Niveles --</option>
<option value="info" <?php echo $nivel === 'info' ? 'selected' : ''; ?>>Info</option>
<option value="warning" <?php echo $nivel === 'warning' ? 'selected' : ''; ?>>Warning</option>
<option value="error" <?php echo $nivel === 'error' ? 'selected' : ''; ?>>Error</option>
</select>
<select name="origen" class="form-control">
<option value="">-- Todos los Orígenes --</option>
<option value="sistema" <?php echo $origen === 'sistema' ? 'selected' : ''; ?>>Sistema</option>
<option value="usuario" <?php echo $origen === 'usuario' ? 'selected' : ''; ?>>Usuario</option>
<option value="bot" <?php echo $origen === 'bot' ? 'selected' : ''; ?>>Bot</option>
<option value="webhook" <?php echo $origen === 'webhook' ? 'selected' : ''; ?>>Webhook</option>
</select>
<button type="submit" class="btn btn-primary">
<i class="fas fa-filter"></i> Filtrar
</button>
<?php if ($nivel || $origen || $search): ?>
<a href="list.php" class="btn btn-secondary">Limpiar</a>
<?php endif; ?>
</form>
<table>
<thead>
<tr>
<th>Fecha</th>
<th>Nivel</th>
<th>Origen</th>
<th>Descripción</th>
<th>Usuario</th>
<th>Detalles</th>
</tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr><td colspan="6" style="text-align:center; color:#666;">No hay registros encontrados.</td></tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td style="font-size: 14px; color: #666;"><?php echo $log['fecha']; ?></td>
<td>
<span class="badge badge-<?php echo $log['nivel']; ?>">
<?php echo strtoupper($log['nivel']); ?>
</span>
</td>
<td><?php echo ucfirst($log['origen']); ?></td>
<td><?php echo htmlspecialchars(substr($log['descripcion'], 0, 100)) . (strlen($log['descripcion']) > 100 ? '...' : ''); ?></td>
<td><?php echo $log['username'] ? htmlspecialchars($log['username']) : '-'; ?></td>
<td>
<?php if ($log['datos_json']): ?>
<button class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px;"
onclick='showDetails(<?php echo json_encode($log['datos_json']); ?>)'>
<i class="fas fa-code"></i> JSON
</button>
<?php else: ?>
<span style="color:#ccc;">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<!-- Paginación -->
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?page=<?php echo $i; ?>&nivel=<?php echo $nivel; ?>&origen=<?php echo $origen; ?>&search=<?php echo $search; ?>"
class="<?php echo $i === $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Modal Detalles -->
<div id="detailsModal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="closeModal()">&times;</span>
<h2>Detalles del Log</h2>
<pre id="jsonContent"></pre>
</div>
</div>
<script>
function showDetails(jsonString) {
try {
// Si ya es objeto, usarlo, si es string, parsearlo
const obj = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
document.getElementById('jsonContent').textContent = JSON.stringify(obj, null, 2);
document.getElementById('detailsModal').style.display = 'block';
} catch (e) {
console.error(e);
alert('Error al parsear JSON');
}
}
function closeModal() {
document.getElementById('detailsModal').style.display = 'none';
}
window.onclick = function(event) {
if (event.target == document.getElementById('detailsModal')) {
closeModal();
}
}
</script>
</body>
</html>

736
discord/views/messages/create.php Executable file
View File

@@ -0,0 +1,736 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
// Verificar permiso para enviar mensajes
if (!hasPermission('send_messages', 'discord')) {
die('No tienes permiso para crear y enviar mensajes de Discord.');
}
$db = getDB();
// Obtener plantillas para el selector
$stmt = $db->query("SELECT id, nombre, contenido FROM plantillas_discord ORDER BY nombre ASC");
$plantillas = $stmt->fetchAll();
// Obtener destinatarios guardados (si existen)
$stmt = $db->query("SELECT id, nombre, discord_id as identificador, tipo FROM destinatarios_discord ORDER BY nombre ASC");
$destinatarios = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crear Mensaje Discord - Sistema de Bots</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/4.1.5/css/flag-icons.min.css" />
<style>
/* Estilos para los botones de traducción */
.translation-buttons {
margin-bottom: 10px;
padding: 5px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #f8f9fa;
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.translation-buttons .btn-translate {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
background: white;
transition: all 0.2s;
}
.translation-buttons .btn-translate:hover {
background-color: #e9ecef;
transform: translateY(-1px);
}
.translation-buttons .btn-translate:active {
transform: translateY(0);
}
.translation-buttons .flag-icon {
width: 16px;
height: 12px;
border-radius: 2px;
box-shadow: 0 0 1px rgba(0,0,0,0.3);
}
.translation-buttons .loading {
opacity: 0.7;
pointer-events: none;
}
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.form-container {
background: white;
border-radius: 15px;
padding: 40px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 15px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--discord-color);
}
.form-help {
font-size: 13px;
color: #666;
margin-top: 5px;
}
.btn {
padding: 12px 24px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 600;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.btn-primary:hover {
background: var(--discord-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-success {
background: #28a745;
color: white;
}
.form-actions {
display: flex;
gap: 15px;
margin-top: 30px;
border-top: 1px solid #eee;
padding-top: 20px;
}
/* Select2 Customization */
.select2-container--default .select2-selection--single {
height: 45px;
border: 2px solid #e0e0e0;
border-radius: 8px;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 45px;
padding-left: 15px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 43px;
}
/* Modal Galería */
.modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: white;
margin: 50px auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 900px;
max-height: 80vh;
overflow-y: auto;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.gallery-item {
cursor: pointer;
border: 3px solid transparent;
border-radius: 10px;
overflow: hidden;
transition: all 0.3s;
}
.gallery-item:hover {
border-color: var(--discord-color);
transform: scale(1.05);
}
.gallery-item img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.close {
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-paper-plane"></i> Crear Mensaje Discord</h1>
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
<div class="container">
<div class="form-container">
<form id="messageForm">
<div class="form-group">
<label for="destinatario_id">Destinatarios (Canal ID o Usuario ID) *</label>
<div style="display: flex; gap: 10px;">
<div style="flex-grow: 1;">
<select id="destinatario_select" class="form-control" style="width: 100%;" multiple="multiple">
<?php foreach ($destinatarios as $dest): ?>
<option value="<?php echo htmlspecialchars($dest['identificador']); ?>">
<?php echo htmlspecialchars($dest['nombre']); ?> (<?php echo $dest['tipo']; ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div style="flex-grow: 1;">
<input type="text" id="destinatario_manual" name="destinatario_manual" class="form-control" placeholder="O pega IDs manualmente aquí, separados por comas">
</div>
</div>
<div class="form-help">Selecciona uno o varios destinatarios guardados, o ingresa IDs manualmente separados por comas.</div>
</div>
<div class="form-group">
<label for="plantilla_id">Cargar Plantilla (Opcional)</label>
<select id="plantilla_id" class="form-control" onchange="loadTemplate(this.value)">
<option value="">-- Seleccionar Plantilla --</option>
<?php foreach ($plantillas as $plantilla): ?>
<option value="<?php echo htmlspecialchars($plantilla['id']); ?>">
<?php echo htmlspecialchars($plantilla['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="contenido">Contenido del Mensaje *</label>
<div id="translation-buttons" class="translation-buttons" style="margin-bottom: 10px; display: flex; gap: 5px;">
<!-- Los botones se cargarán dinámicamente con JavaScript -->
</div>
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px; padding: 5px 10px; font-size: 12px;">
<i class="fas fa-images"></i> Insertar Imagen
</button>
<textarea id="contenido" name="contenido" class="form-control" rows="10" required></textarea>
</div>
<div class="form-group">
<label>Tipo de Envío *</label>
<div>
<input type="radio" id="send_immediate" name="send_type" value="inmediato" checked>
<label for="send_immediate">Inmediato</label>
<input type="radio" id="send_scheduled" name="send_type" value="programado" class="ml-3">
<label for="send_scheduled">Programado</label>
<input type="radio" id="send_recurring" name="send_type" value="recurrente" class="ml-3">
<label for="send_recurring">Recurrente</label>
</div>
</div>
<div id="scheduled_options" class="form-group" style="display: none;">
<label for="schedule_datetime">Fecha y Hora de Envío *</label>
<input type="datetime-local" id="schedule_datetime" name="schedule_datetime" class="form-control">
</div>
<div id="recurring_options" class="form-group" style="display: none;">
<label for="recurrence_frequency">Frecuencia de Recurrencia *</label>
<select id="recurrence_frequency" name="recurrence_frequency" class="form-control">
<option value="diario">Diario</option>
<option value="semanal">Semanal</option>
<option value="mensual">Mensual</option>
</select>
<div id="recurring_details" style="margin-top: 15px;">
<!-- Detalles específicos de recurrencia (día de la semana, día del mes) -->
<div id="weekly_options" style="display: none;">
<label for="recurrence_day_of_week">Día de la Semana</label>
<select id="recurrence_day_of_week" name="recurrence_day_of_week" class="form-control">
<option value="1">Lunes</option>
<option value="2">Martes</option>
<option value="3">Miércoles</option>
<option value="4">Jueves</option>
<option value="5">Viernes</option>
<option value="6">Sábado</option>
<option value="7">Domingo</option>
</select>
</div>
<div id="monthly_options" style="display: none;">
<label for="recurrence_day_of_month">Día del Mes</label>
<input type="number" id="recurrence_day_of_month" name="recurrence_day_of_month" class="form-control" min="1" max="31">
</div>
<label for="recurrence_time">Hora de Envío</label>
<input type="time" id="recurrence_time" name="recurrence_time" class="form-control">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="btnEnviar">
<i class="fas fa-paper-plane"></i> Enviar Ahora
</button>
<button type="button" class="btn btn-secondary" onclick="previewMessage()">
<i class="fas fa-eye"></i> Vista Previa
</button>
</div>
</form>
</div>
</div>
<!-- Modal Galería -->
<div id="galleryModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeGallery()">&times;</span>
<h2>Galería de Imágenes</h2>
<div class="gallery-grid" id="galleryGrid"></div>
</div>
</div>
<!-- Templates Data Hidden -->
<script>
const templates = <?php echo json_encode($plantillas); ?>;
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
// Función para cargar los idiomas activos
function loadActiveLanguages() {
return fetch('/shared/languages/get_active.php')
.then(response => response.json())
.then(data => {
if (data.success && data.languages) {
return data.languages;
}
return [];
})
.catch(error => {
console.error('Error cargando idiomas:', error);
return [];
});
}
// Función para crear botones de traducción
function createTranslationButtons(languages) {
const container = $('#translation-buttons');
container.empty();
languages.forEach(lang => {
if (lang.codigo !== 'es') { // No mostramos el botón para español (idioma original)
const button = $(`
<button type="button" class="btn btn-sm btn-outline-secondary translate-btn"
data-lang="${lang.codigo}"
title="Traducir a ${lang.nombre}">
<span class="flag-icon flag-icon-${lang.bandera || 'globe'}"></span>
</button>
`);
container.append(button);
}
});
// Manejador de eventos para los botones de traducción
$('.translate-btn').on('click', async function() {
const targetLang = $(this).data('lang');
const content = $('#contenido').val();
if (!content) {
alert('Por favor, ingrese un mensaje para traducir.');
return;
}
try {
const response = await fetch('/shared/translations/translate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: content,
target: targetLang,
source: 'es' // Asumimos que el texto original está en español
})
});
const data = await response.json();
if (data.success) {
// Agregar la traducción al final del contenido
const translatedText = `\n\n--- Traducción a ${targetLang.toUpperCase()} ---\n${data.translatedText}`;
$('#contenido').val(content + translatedText);
// Eliminar el botón de traducción
$(this).remove();
} else {
alert('Error al traducir: ' + (data.error || 'Error desconocido'));
}
} catch (error) {
console.error('Error al traducir:', error);
alert('Error al conectar con el servicio de traducción.');
}
});
}
// Cargar los botones de traducción al iniciar
$(document).ready(function() {
loadActiveLanguages().then(languages => {
createTranslationButtons(languages);
});
$('#contenido').summernote({
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
]
});
// Inicializar Select2 para selección múltiple
$('#destinatario_select').select2({
placeholder: "-- Seleccionar Destinatarios Guardados --",
allowClear: true // Permite deseleccionar
});
// Sincronizar select con input manual
$('#destinatario_select').on('change', function() {
const selectedIds = $(this).val(); // Array de IDs del select2
const manualIds = $('#destinatario_manual').val().split(',').map(id => id.trim()).filter(id => id !== '');
// Combinar y eliminar duplicados
const combinedIds = [...new Set([...selectedIds, ...manualIds])];
// Actualizar el campo manual para reflejar todas las selecciones y entradas manuales
$('#destinatario_manual').val(combinedIds.join(', '));
});
$('#destinatario_manual').on('input', function() {
const manualIds = $(this).val().split(',').map(id => id.trim()).filter(id => id !== '');
const selectedIds = $('#destinatario_select').val(); // IDs del select2
// Asegurarse de que el select2 no se deseleccione si se añade manualmente
// Esto es complejo si se quiere mantener el estado exacto en ambos sentidos.
// Para simplificar, solo aseguramos que el input manual tenga todos los IDs.
const combinedIds = [...new Set([...manualIds, ...selectedIds])];
// Intentar seleccionar en select2 lo que está en manual si existe
$('#destinatario_select').val(manualIds).trigger('change');
});
});
function loadTemplate(id) {
if (!id) return;
const template = templates.find(t => t.id == id);
if (template) {
if (confirm('¿Reemplazar el contenido actual con la plantilla?')) {
$('#contenido').summernote('code', template.contenido);
}
}
}
function openGallery() {
$('#galleryModal').show();
loadGalleryImages();
}
function closeGallery() {
$('#galleryModal').hide();
}
async function loadGalleryImages() {
try {
const response = await fetch('/gallery/api/list.php');
const data = await response.json();
const grid = document.getElementById('galleryGrid');
if (data.success && data.images.length > 0) {
grid.innerHTML = data.images.map(img => `
<div class="gallery-item" onclick="insertImage('${img.url}')">
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
</div>
`).join('');
} else {
grid.innerHTML = '<p>No hay imágenes.</p>';
}
} catch (e) {
console.error(e);
}
}
function insertImage(url) {
const fullUrl = window.location.origin + url;
$('#contenido').summernote('insertImage', fullUrl);
closeGallery();
}
function previewMessage() {
const content = $('#contenido').summernote('code');
const win = window.open('', 'Preview', 'width=800,height=600');
win.document.write('<div style="padding:20px;font-family:sans-serif;">' + content + '</div>');
}
// Cerrar modal click fuera
window.onclick = function(event) {
if (event.target == document.getElementById('galleryModal')) {
closeGallery();
}
}
// Lógica para mostrar/ocultar campos de programación/recurrencia
const sendTypeRadios = document.querySelectorAll('input[name="send_type"]');
const scheduledOptions = document.getElementById('scheduled_options');
const recurringOptions = document.getElementById('recurring_options');
const recurrenceFrequency = document.getElementById('recurrence_frequency');
const weeklyOptions = document.getElementById('weekly_options');
const monthlyOptions = document.getElementById('monthly_options');
function toggleSendTypeOptions() {
const selectedSendType = document.querySelector('input[name="send_type"]:checked').value;
scheduledOptions.style.display = 'none';
recurringOptions.style.display = 'none';
if (selectedSendType === 'programado') {
scheduledOptions.style.display = 'block';
} else if (selectedSendType === 'recurrente') {
recurringOptions.style.display = 'block';
toggleRecurringDetails(); // Mostrar detalles específicos al cargar
}
}
function toggleRecurringDetails() {
const selectedFrequency = recurrenceFrequency.value;
weeklyOptions.style.display = 'none';
monthlyOptions.style.display = 'none';
if (selectedFrequency === 'semanal') {
weeklyOptions.style.display = 'block';
} else if (selectedFrequency === 'mensual') {
monthlyOptions.style.display = 'block';
}
}
// Inicializar al cargar la página
toggleSendTypeOptions();
// Escuchar cambios en los tipos de envío
sendTypeRadios.forEach(radio => {
radio.addEventListener('change', toggleSendTypeOptions);
});
// Escuchar cambios en la frecuencia de recurrencia
recurrenceFrequency.addEventListener('change', toggleRecurringDetails);
$('#messageForm').on('submit', async function(e) {
e.preventDefault();
const selectedDestinatarios = $('#destinatario_select').val() || []; // Array de IDs del select2
const manualDestinatarios = $('#destinatario_manual').val()
.split(',')
.map(id => id.trim())
.filter(id => id !== '');
// Combinar y eliminar duplicados de ambos orígenes
const destinatarios = [...new Set([...selectedDestinatarios, ...manualDestinatarios])];
const contenido = $('#contenido').summernote('code');
const sendType = document.querySelector('input[name="send_type"]:checked').value;
let scheduleData = {};
if (destinatarios.length === 0 || !contenido) {
alert('Por favor selecciona al menos un destinatario y escribe un mensaje.');
return;
}
if (sendType === 'programado') {
const scheduleDatetime = $('#schedule_datetime').val();
if (!scheduleDatetime) {
alert('Por favor, selecciona la fecha y hora de envío programado.');
return;
}
scheduleData = {
fecha_envio: scheduleDatetime
};
} else if (sendType === 'recurrente') {
const recurrenceFrequencyVal = $('#recurrence_frequency').val();
const recurrenceTime = $('#recurrence_time').val();
if (!recurrenceFrequencyVal || !recurrenceTime) {
alert('Por favor, completa la frecuencia y hora de envío recurrente.');
return;
}
scheduleData = {
frecuencia: recurrenceFrequencyVal,
hora_envio: recurrenceTime
};
if (recurrenceFrequencyVal === 'semanal') {
const dayOfWeek = $('#recurrence_day_of_week').val();
if (!dayOfWeek) {
alert('Por favor, selecciona el día de la semana para el envío recurrente.');
return;
}
scheduleData.dia_semana = dayOfWeek;
} else if (recurrenceFrequencyVal === 'mensual') {
const dayOfMonth = $('#recurrence_day_of_month').val();
if (!dayOfMonth || dayOfMonth < 1 || dayOfMonth > 31) {
alert('Por favor, ingresa un día válido del mes (1-31) para el envío recurrente.');
return;
}
scheduleData.dia_mes = dayOfMonth;
}
}
if (!confirm(`¿Enviar mensaje (${sendType}) a ${destinatarios.length} destinatario(s)?`)) return;
$('#btnEnviar').prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Enviando...');
try {
const response = await fetch('/discord/api/messages/send.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
destinatario_id: destinatarios, // Ahora puede ser un array
contenido: contenido,
tipo_envio: sendType,
...scheduleData // Añadir datos de programación/recurrencia
})
});
const result = await response.json();
if (result.success) {
alert('¡Mensaje enviado correctamente!');
window.location.href = '/discord/views/messages/sent.php';
} else {
alert('Error: ' + (result.error || 'Error desconocido'));
}
} catch (error) {
console.error(error);
alert('Error de conexión');
} finally {
$('#btnEnviar').prop('disabled', false).html('<i class="fas fa-paper-plane"></i> Enviar Ahora');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,39 @@
[29-Nov-2025 04:52:45 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
Stack trace:
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
[29-Nov-2025 04:53:24 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
Stack trace:
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
[29-Nov-2025 04:53:28 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
Stack trace:
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
[29-Nov-2025 04:54:04 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'identificador' in 'field list' in /var/www/html/bot/discord/views/messages/create.php:21
Stack trace:
#0 /var/www/html/bot/discord/views/messages/create.php(21): PDO->query()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/create.php on line 21
[30-Nov-2025 15:23:38 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
Stack trace:
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
[30-Nov-2025 15:24:23 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
Stack trace:
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
[30-Nov-2025 15:25:34 America/Mexico_City] PHP Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in /var/www/html/bot/discord/views/messages/sent.php:51
Stack trace:
#0 /var/www/html/bot/discord/views/messages/sent.php(51): PDOStatement->execute()
#1 {main}
thrown in /var/www/html/bot/discord/views/messages/sent.php on line 51
[30-Nov-2025 16:17:08 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
[30-Nov-2025 16:19:47 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
[30-Nov-2025 16:20:43 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283
[30-Nov-2025 16:22:34 America/Mexico_City] PHP Deprecated: strtotime(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/html/bot/discord/views/messages/sent.php on line 283

377
discord/views/messages/sent.php Executable file
View File

@@ -0,0 +1,377 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
$db = getDB();
// Paginación
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = 10;
$offset = ($page - 1) * $limit;
// Obtener estado de filtro si existe
$filterStatus = $_GET['status'] ?? 'todos'; // Default a 'todos'
// Construir la cláusula WHERE para filtrar por estado
$whereClause = "WHERE m.estado != 'deshabilitado'"; // No mostrar deshabilitados por defecto en la lista principal
$params = [];
if ($filterStatus !== 'todos') {
$whereClause = "WHERE m.estado = ?";
$params[] = $filterStatus;
}
// Obtener total de mensajes (según filtro)
$stmt = $db->prepare("SELECT COUNT(*) FROM mensajes_discord m {$whereClause}");
$stmt->execute($params);
$totalMessages = $stmt->fetchColumn();
$totalPages = ceil($totalMessages / $limit);
// Obtener mensajes (según filtro)
$stmt = $db->prepare("
SELECT m.*, u.username
FROM mensajes_discord m
LEFT JOIN usuarios u ON m.usuario_id = u.id
{$whereClause}
ORDER BY m.fecha_envio DESC
LIMIT ? OFFSET ?
");
$stmt->bindValue(count($params) + 1, $limit, PDO::PARAM_INT);
$stmt->bindValue(count($params) + 2, $offset, PDO::PARAM_INT);
// Bind WHERE parameters manually
foreach ($params as $k => $v) {
$stmt->bindValue($k + 1, $v);
}
$stmt->execute();
$mensajes = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mensajes - Discord</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.messages-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.message-card {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.message-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
.message-dest {
font-weight: 600;
color: var(--discord-color);
background: #eef0ff;
padding: 2px 8px;
border-radius: 10px;
}
.message-status {
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
text-transform: capitalize;
color: white;
}
.status-enviado { background-color: #28a745; }
.status-pendiente { background-color: #ffc107; }
.status-fallido { background-color: #dc3545; }
.status-deshabilitado { background-color: #6c757d; }
.message-content {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 10px;
font-family: monospace;
white-space: pre-wrap;
max-height: 100px;
overflow-y: auto;
}
.message-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #999;
}
.btn {
padding: 8px 16px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 30px;
}
.page-link {
background: white;
color: var(--discord-color);
padding: 8px 12px;
border-radius: 5px;
text-decoration: none;
font-weight: 600;
}
.page-link.active {
background: var(--discord-color);
color: white;
}
.empty-state {
background: white;
border-radius: 15px;
padding: 40px;
text-align: center;
color: #666;
}
.filter-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
}
.filter-buttons .btn {
padding: 8px 15px;
font-size: 13px;
border: 1px solid #ddd;
color: #333;
background-color: #f8f9fa;
}
.filter-buttons .btn.active {
background-color: var(--discord-color);
color: white;
border-color: var(--discord-color);
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-history"></i> Historial de Mensajes Discord</h1>
<div style="display: flex; gap: 10px;">
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
<a href="create.php" class="btn btn-primary">
<i class="fas fa-plus"></i> Nuevo Mensaje
</a>
</div>
</div>
<div class="container">
<div class="filter-buttons">
<a href="?status=todos" class="btn <?php echo $filterStatus === 'todos' ? 'active' : ''; ?>">Todos</a>
<a href="?status=enviado" class="btn <?php echo $filterStatus === 'enviado' ? 'active' : ''; ?>">Enviados</a>
<a href="?status=pendiente" class="btn <?php echo $filterStatus === 'pendiente' ? 'active' : ''; ?>">Pendientes</a>
<a href="?status=fallido" class="btn <?php echo $filterStatus === 'fallido' ? 'active' : ''; ?>">Fallidos</a>
<a href="?status=deshabilitado" class="btn <?php echo $filterStatus === 'deshabilitado' ? 'active' : ''; ?>">Deshabilitados</a>
</div>
<?php if (empty($mensajes)): ?>
<div class="empty-state">
<i class="fas fa-inbox" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
<h2>No hay mensajes en este estado</h2>
<p>Los mensajes que cumplan este criterio aparecerán aquí.</p>
</div>
<?php else: ?>
<div class="messages-list">
<?php foreach ($mensajes as $msg): ?>
<div class="message-card">
<div class="message-header">
<div>
Enviado a: <span class="message-dest"><?php echo htmlspecialchars($msg['canal_id']); ?></span>
</div>
<div>
<span class="message-status status-<?php echo htmlspecialchars($msg['estado']); ?>">
<?php echo htmlspecialchars($msg['estado']); ?>
</span>
<i class="fas fa-clock" style="margin-left: 10px;"></i> <?php echo date('d/m/Y H:i', strtotime($msg['fecha_envio'] ?? 'now')); ?>
</div>
</div>
<div class="message-content"><?php echo strip_tags($msg['contenido']); ?></div>
<div class="message-footer">
<div>
<i class="fas fa-user"></i> Por: <?php echo htmlspecialchars($msg['username'] ?? 'Sistema'); ?>
</div>
<div>
ID Discord: <?php echo htmlspecialchars($msg['mensaje_discord_id'] ?? 'N/A'); ?>
<button class="btn btn-warning btn-sm" onclick="retryMessage(<?php echo $msg['id']; ?>)" style="margin-left: 15px;">
<i class="fas fa-sync-alt"></i> Reintentar
</button>
<button class="btn btn-danger btn-sm" onclick="deleteMessage(<?php echo $msg['id']; ?>)" style="margin-left: 5px;">
<i class="fas fa-trash"></i> Eliminar
</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?page=<?php echo $i; ?>&status=<?php echo htmlspecialchars($filterStatus); ?>" class="page-link <?php echo $i === $page ? 'active' : ''; ?>">
<?php echo $i; ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<script>
async function retryMessage(messageId) {
if (!confirm('¿Estás seguro de que quieres volver a poner este mensaje en la cola como "pendiente"?')) {
return;
}
try {
const response = await fetch('/discord/api/messages/retry.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: messageId })
});
const result = await response.json();
if (result.success) {
alert('Mensaje marcado como pendiente.');
location.reload();
} else {
alert('Error al reintentar el mensaje: ' + (result.error || 'Error desconocido.'));
}
} catch (error) {
console.error('Error al enviar la solicitud de reintento:', error);
alert('Error de conexión al intentar reintentar el mensaje.');
}
}
async function deleteMessage(messageId) {
if (!confirm('¿Estás seguro de que quieres eliminar/deshabilitar este mensaje?')) {
return;
}
try {
const response = await fetch('/discord/api/messages/delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: messageId })
});
const result = await response.json();
if (result.success) {
alert('Mensaje deshabilitado correctamente.');
location.reload(); // Recargar la página para actualizar la lista
} else {
alert('Error al deshabilitar el mensaje: ' + (result.error || 'Error desconocido.'));
}
} catch (error) {
console.error('Error al enviar la solicitud de eliminación:', error);
alert('Error de conexión al intentar deshabilitar el mensaje.');
}
}
</script>
</body>
</html>

461
discord/views/recipients/list.php Executable file
View File

@@ -0,0 +1,461 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
// Verificar permiso para ver la página de destinatarios
if (!hasPermission('view_recipients', 'discord')) {
die('No tienes permiso para ver los destinatarios de Discord.');
}
$db = getDB();
// Obtener destinatarios
$stmt = $db->query("SELECT * FROM destinatarios_discord ORDER BY nombre ASC");
$destinatarios = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Destinatarios Discord - Sistema de Bots</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.table-responsive {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
font-weight: 600;
color: #555;
background: #f8f9fa;
}
.badge {
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: 600;
}
.badge-channel {
background: #e3e7ff;
color: var(--discord-color);
}
.badge-user {
background: #d4edda;
color: #155724;
}
.badge-grupo {
background: #fff3cd;
color: #856404;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.btn-primary:hover {
background: var(--discord-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
padding: 5px 10px;
font-size: 12px;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
background: white;
margin: 10% auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 500px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
.form-control {
width: 100%;
padding: 10px;
border: 2px solid #eee;
border-radius: 8px;
}
.close {
float: right;
font-size: 24px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-users"></i> Destinatarios Discord</h1>
<div style="display: flex; gap: 10px;">
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
<?php if (hasPermission('manage_recipients', 'discord')): ?>
<button onclick="openCreateModal()" class="btn btn-primary">
<i class="fas fa-plus"></i> Nuevo Destinatario
</button>
<?php endif; ?>
</div>
</div>
<div class="container">
<div class="card">
<?php if (empty($destinatarios)): ?>
<div style="text-align: center; padding: 40px; color: #666;">
<i class="fas fa-users-slash" style="font-size: 48px; margin-bottom: 20px; color: #ddd;"></i>
<h3>No hay destinatarios guardados</h3>
<p>Agrega canales o usuarios frecuentes para enviar mensajes más rápido.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Tipo</th>
<th>ID Discord</th>
<th>Fecha Registro</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($destinatarios as $dest): ?>
<tr>
<td>
<strong><?php echo htmlspecialchars($dest['nombre']); ?></strong>
</td>
<td>
<span class="badge <?php
if ($dest['tipo'] == 'canal') echo 'badge-channel';
else if ($dest['tipo'] == 'usuario') echo 'badge-user';
else if ($dest['tipo'] == 'grupo') echo 'badge-grupo';
?>">
<?php echo ucfirst($dest['tipo']); ?>
</span>
</td>
<td><code><?php echo htmlspecialchars($dest['discord_id']); ?></code></td>
<td><?php echo date('d/m/Y', strtotime($dest['fecha_registro'])); ?></td>
<td>
<?php if (hasPermission('manage_recipients', 'discord')): ?>
<button onclick="editRecipient(<?php echo $dest['id']; ?>, '<?php echo htmlspecialchars($dest['nombre'], ENT_QUOTES); ?>', '<?php echo htmlspecialchars($dest['discord_id'], ENT_QUOTES); ?>', '<?php echo htmlspecialchars($dest['tipo'], ENT_QUOTES); ?>')" class="btn btn-primary" style="padding: 5px 10px; font-size: 12px;">
<i class="fas fa-edit"></i>
</button>
<?php if ($dest['tipo'] === 'usuario'): ?>
<button onclick="kickRecipient(<?php echo $dest['id']; ?>)" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">
<i class="fas fa-user-slash"></i> Expulsar
</button>
<?php endif; ?>
<button onclick="deleteRecipient(<?php echo $dest['id']; ?>)" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;">
<i class="fas fa-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Modal Crear -->
<div id="createModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeCreateModal()">&times;</span>
<h2 style="margin-bottom: 20px;">Nuevo Destinatario</h2>
<form id="createForm">
<div class="form-group">
<label>Nombre (Alias)</label>
<input type="text" name="nombre" class="form-control" required placeholder="Ej: Canal General">
</div>
<div class="form-group">
<label>ID de Discord</label>
<input type="text" name="discord_id" class="form-control" required placeholder="Ej: 123456789012345678">
</div>
<div class="form-group">
<label>Tipo</label>
<select name="tipo" class="form-control">
<option value="canal">Canal</option>
<option value="usuario">Usuario (DM)</option>
<option value="grupo">Grupo</option>
</select>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Guardar</button>
</form>
</div>
</div>
<!-- Modal Editar -->
<div id="editModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span>
<h2 style="margin-bottom: 20px;">Editar Destinatario</h2>
<form id="editForm">
<input type="hidden" id="edit_id" name="id">
<div class="form-group">
<label>Nombre (Alias)</label>
<input type="text" id="edit_nombre" name="nombre" class="form-control" required>
</div>
<div class="form-group">
<label>ID de Discord</label>
<input type="text" id="edit_discord_id" name="discord_id" class="form-control" required>
</div>
<div class="form-group">
<label>Tipo</label>
<select id="edit_tipo" name="tipo" class="form-control">
<option value="canal">Canal</option>
<option value="usuario">Usuario (DM)</option>
<option value="grupo">Grupo</option>
</select>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Guardar Cambios</button>
</form>
</div>
</div>
<script>
function openCreateModal() {
document.getElementById('createModal').style.display = 'block';
}
function closeCreateModal() {
document.getElementById('createModal').style.display = 'none';
}
function openEditModal() {
document.getElementById('editModal').style.display = 'block';
}
function closeEditModal() {
document.getElementById('editModal').style.display = 'none';
}
function editRecipient(id, nombre, discord_id, tipo) {
document.getElementById('edit_id').value = id;
document.getElementById('edit_nombre').value = nombre;
document.getElementById('edit_discord_id').value = discord_id;
document.getElementById('edit_tipo').value = tipo;
openEditModal();
}
document.getElementById('createForm').onsubmit = async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch('/discord/api/recipients/create.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert('Error: ' + result.error);
}
} catch (error) {
console.error(error);
alert('Error de conexión');
}
};
document.getElementById('editForm').onsubmit = async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch('/discord/api/recipients/edit.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert('Error: ' + result.error);
}
} catch (error) {
console.error(error);
alert('Error de conexión');
}
};
async function deleteRecipient(id) {
if (!confirm('¿Eliminar este destinatario? Esta acción es solo local.')) return;
try {
const response = await fetch('/discord/api/recipients/delete.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id})
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert('Error: ' + result.error);
}
} catch (error) {
console.error(error);
alert('Error de conexión');
}
}
async function kickRecipient(id) {
if (!confirm('¿Estás seguro de expulsar este usuario de Discord? Esta acción es irreversible en Discord.')) return;
try {
const response = await fetch('/discord/api/recipients/kick.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id})
});
const result = await response.json();
if (result.success) {
alert('Usuario expulsado de Discord y eliminado de la lista local.');
location.reload();
} else {
alert('Error al expulsar: ' + result.error);
}
} catch (error) {
console.error(error);
alert('Error de conexión');
}
}
window.onclick = function(event) {
if (event.target == document.getElementById('createModal')) {
closeCreateModal();
}
if (event.target == document.getElementById('editModal')) {
closeEditModal();
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
[29-Nov-2025 21:47:58 America/Mexico_City] PHP Fatal error: Cannot redeclare hasPermission() (previously declared in /var/www/html/bot/shared/utils/helpers.php:97) in /var/www/html/bot/shared/auth/jwt.php on line 216

View File

@@ -0,0 +1,529 @@
<?php
session_start();
// Cargar variables de entorno
if (file_exists(__DIR__ . '/../../../.env')) {
$lines = file(__DIR__ . '/../../../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$_ENV[trim($key)] = trim($value);
}
}
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
$userData = JWTAuth::requireAuth();
// Verificar permiso para ver y crear plantillas
if (!hasPermission('editar_plantillas')) {
die('No tienes permiso para crear plantillas de Discord.');
}
// PHP logic for initial display, not for form processing
$error = $_GET['error'] ?? '';
$success = $_GET['success'] ?? '';
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crear Plantilla - Discord</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.form-container {
background: white;
border-radius: 15px;
padding: 40px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
.form-group input[type="text"] {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 15px;
transition: border-color 0.3s;
}
.form-group input[type="text"]:focus {
outline: none;
border-color: var(--discord-color);
}
.form-help {
font-size: 13px;
color: #666;
margin-top: 5px;
}
.alert {
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: #fee;
color: #c33;
border-left: 4px solid #c33;
}
.alert-success {
background: #efe;
color: #3c3;
border-left: 4px solid #3c3;
}
.btn {
padding: 12px 24px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 600;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.btn-primary:hover {
background: var(--discord-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.form-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
.note-editor {
border: 2px solid #e0e0e0;
border-radius: 8px;
}
.note-editor.note-frame {
border-color: var(--discord-color);
}
/* Modal de galería */
.modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: white;
margin: 50px auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 900px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
color: var(--discord-color);
}
.close {
font-size: 28px;
font-weight: bold;
cursor: pointer;
color: #999;
}
.close:hover {
color: #333;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.gallery-item {
cursor: pointer;
border: 3px solid transparent;
border-radius: 10px;
overflow: hidden;
transition: all 0.3s;
}
.gallery-item:hover {
border-color: var(--discord-color);
transform: scale(1.05);
}
.gallery-item img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 10px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: var(--discord-color);
background: #f8f9fa;
}
.upload-area input[type="file"] {
display: none;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-plus"></i> Crear Plantilla Discord</h1>
<a href="list.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
<div class="container">
<div class="form-container">
<div id="alert-messages">
<?php if ($error): ?>
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
<?php endif; ?>
</div>
<form id="createTemplateForm">
<div class="form-group">
<label for="nombre">Nombre de la Plantilla *</label>
<input type="text" id="nombre" name="nombre" required value="">
<div class="form-help">Nombre descriptivo para identificar la plantilla</div>
</div>
<div class="form-group">
<label for="comando">Comando (opcional)</label>
<input type="text" id="comando" name="comando" placeholder="Ej: /comandos, #asedio" value="">
<div class="form-help">Comando para invocar esta plantilla en Discord. Debe ser único.</div>
</div>
<div class="form-group">
<label for="contenido">Contenido *</label>
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px;">
<i class="fas fa-images"></i> Insertar Imagen
</button>
<textarea id="contenido" name="contenido"></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Guardar Plantilla
</button>
<button type="button" onclick="previewContent()" class="btn btn-secondary">
<i class="fas fa-eye"></i> Vista Previa
</button>
<a href="list.php" class="btn btn-secondary">
<i class="fas fa-times"></i> Cancelar
</a>
</div>
</form>
</div>
</div>
<!-- Modal de Galería -->
<div id="galleryModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2><i class="fas fa-images"></i> Galería de Imágenes</h2>
<span class="close" onclick="closeGallery()">&times;</span>
</div>
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #ddd;"></i>
<p style="margin-top: 10px; color: #666;">Haz clic para subir una imagen o arrastra aquí</p>
<input type="file" id="fileInput" accept="image/*" onchange="uploadImage(this)">
</div>
<div class="gallery-grid" id="galleryGrid">
<p style="text-align: center; color: #999;">Cargando imágenes...</p>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<script>
console.log('Scripts loaded');
console.log('jQuery:', typeof $);
console.log('Summernote:', typeof $.fn.summernote);
$(document).ready(function() {
console.log('Document ready');
console.log('Textarea exists:', $('#contenido').length);
try {
$('#contenido').summernote({
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
],
placeholder: 'Escribe el contenido de tu plantilla aquí...'
});
console.log('Summernote initialized successfully');
} catch (error) {
console.error('Error initializing Summernote:', error);
}
// Handle form submission via Fetch API
$('#createTemplateForm').on('submit', async function(e) {
e.preventDefault(); // Prevent default form submission
const nombre = $('#nombre').val();
const comando = $('#comando').val();
const contenido = $('#contenido').summernote('code'); // Get content from Summernote
// Clear previous alerts
$('#alert-messages').empty();
if (!nombre || !contenido) {
showAlert('El nombre y el contenido son obligatorios.', 'error');
return;
}
try {
const response = await fetch('/discord/api/templates/create.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ nombre, comando, contenido })
});
const result = await response.json();
if (result.success) {
showAlert('Plantilla creada correctamente.', 'success');
setTimeout(() => {
window.location.href = 'list.php';
}, 1500); // Redirect after a short delay
} else {
showAlert(result.error || 'Error desconocido al crear la plantilla.', 'error');
}
} catch (error) {
console.error('Error al enviar la solicitud:', error);
showAlert('Error de conexión al guardar la plantilla.', 'error');
}
});
});
function openGallery() {
document.getElementById('galleryModal').style.display = 'block';
loadGallery();
}
function closeGallery() {
document.getElementById('galleryModal').style.display = 'none';
}
async function loadGallery() {
try {
const response = await fetch('/gallery/api/list.php');
const data = await response.json();
const grid = document.getElementById('galleryGrid');
if (data.success && data.images.length > 0) {
grid.innerHTML = data.images.map(img => `
<div class="gallery-item" onclick="insertImage('${img.url}')">
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
</div>
`).join('');
} else {
grid.innerHTML = '<p style="text-align: center; color: #999;">No hay imágenes disponibles</p>';
}
} catch (error) {
console.error('Error cargando galería:', error);
document.getElementById('galleryGrid').innerHTML = '<p style="text-align: center; color: #c33;">Error cargando imágenes</p>';
}
}
function insertImage(url) {
const fullUrl = window.location.origin + url;
$('#contenido').summernote('insertImage', fullUrl);
closeGallery();
}
async function uploadImage(input) {
if (!input.files || !input.files[0]) return;
const formData = new FormData();
formData.append('image', input.files[0]);
try {
const response = await fetch('/gallery/api/upload.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('Imagen subida correctamente');
loadGallery();
} else {
alert('Error: ' + data.error);
}
} catch (error) {
alert('Error al subir la imagen');
console.error(error);
}
input.value = '';
}
function previewContent() {
const content = $('#contenido').summernote('code');
const win = window.open('', 'Preview', 'width=800,height=600');
const html = '<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<title>Vista Previa</title>' +
'<style>' +
'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; background: #f8f9fa; }' +
'.preview-container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }' +
'</style>' +
'</head>' +
'<body>' +
'<div class="preview-container">' +
content +
'</div>' +
'</body>' +
'</html>';
win.document.write(html);
}
// Cerrar modal al hacer clic fuera
window.onclick = function(event) {
const modal = document.getElementById('galleryModal');
if (event.target == modal) {
closeGallery();
}
}
function showAlert(message, type) {
const alertDiv = `<div class="alert alert-${type}">${message}</div>`;
$('#alert-messages').html(alertDiv);
}
</script>
</body>
</html>

544
discord/views/templates/edit.php Executable file
View File

@@ -0,0 +1,544 @@
<?php
session_start();
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
// Verificar permiso para ver y editar plantillas
if (!hasPermission('manage_templates', 'discord')) {
die('No tienes permiso para editar plantillas de Discord.'); // Mensaje de error más general para evitar leaks
}
// Obtener ID de la plantilla
$id = $_GET['id'] ?? null;
if (!$id) {
header('Location: list.php');
exit;
}
$db = getDB();
$stmt = $db->prepare("
SELECT p.*, c.comando
FROM plantillas_discord p
LEFT JOIN comandos_discord c ON p.id = c.plantilla_id
WHERE p.id = ?
");
$stmt->execute([$id]);
$plantilla = $stmt->fetch();
if (!$plantilla) {
header('Location: list.php');
exit;
}
// Verificar propiedad de la plantilla si no es Admin
if ($userData->rol !== 'Admin' && $plantilla['usuario_id'] != $userData->userId) {
die('No tienes permiso para editar esta plantilla, ya que no te pertenece.');
}
$error = $_GET['error'] ?? '';
$success = $_GET['success'] ?? '';
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editar Plantilla - Discord</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.form-container {
background: white;
border-radius: 15px;
padding: 40px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
.form-group input[type="text"] {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 15px;
transition: border-color 0.3s;
}
.form-group input[type="text"]:focus {
outline: none;
border-color: var(--discord-color);
}
.form-help {
font-size: 13px;
color: #666;
margin-top: 5px;
}
.alert {
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: #fee;
color: #c33;
border-left: 4px solid #c33;
}
.alert-success {
background: #efe;
color: #3c3;
border-left: 4px solid #3c3;
}
.btn {
padding: 12px 24px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 600;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.btn-primary:hover {
background: var(--discord-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.form-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
.note-editor {
border: 2px solid #e0e0e0;
border-radius: 8px;
}
.note-editor.note-frame {
border-color: var(--discord-color);
}
.modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: white;
margin: 50px auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 900px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
color: var(--discord-color);
}
.close {
font-size: 28px;
font-weight: bold;
cursor: pointer;
color: #999;
}
.close:hover {
color: #333;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.gallery-item {
cursor: pointer;
border: 3px solid transparent;
border-radius: 10px;
overflow: hidden;
transition: all 0.3s;
}
.gallery-item:hover {
border-color: var(--discord-color);
transform: scale(1.05);
}
.gallery-item img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 10px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: var(--discord-color);
background: #f8f9fa;
}
.upload-area input[type="file"] {
display: none;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-edit"></i> Editar Plantilla</h1>
<a href="list.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
<div class="container">
<div class="form-container">
<div id="alert-messages">
<?php if ($error): ?>
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
<?php endif; ?>
</div>
<form id="editTemplateForm">
<input type="hidden" id="templateId" value="<?php echo htmlspecialchars($plantilla['id']); ?>">
<div class="form-group">
<label for="nombre">Nombre de la Plantilla *</label>
<input type="text" id="nombre" name="nombre" required value="<?php echo htmlspecialchars($plantilla['nombre']); ?>">
<div class="form-help">Nombre descriptivo para identificar la plantilla</div>
</div>
<div class="form-group">
<label for="comando">Comando (opcional)</label>
<input type="text" id="comando" name="comando" placeholder="Ej: /comandos, #asedio" value="<?php echo htmlspecialchars($plantilla['comando'] ?? ''); ?>">
<div class="form-help">Comando para invocar esta plantilla en Discord. Debe ser único.</div>
</div>
<div class="form-group">
<label for="contenido">Contenido *</label>
<button type="button" onclick="openGallery()" class="btn btn-success" style="margin-bottom: 10px;">
<i class="fas fa-images"></i> Insertar Imagen
</button>
<textarea id="contenido" name="contenido"><?php echo htmlspecialchars($plantilla['contenido']); ?></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Guardar Cambios
</button>
<button type="button" onclick="previewContent()" class="btn btn-secondary">
<i class="fas fa-eye"></i> Vista Previa
</button>
<a href="list.php" class="btn btn-secondary">
<i class="fas fa-times"></i> Cancelar
</a>
</div>
</form>
</div>
</div>
<!-- Modal de Galería -->
<div id="galleryModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2><i class="fas fa-images"></i> Galería de Imágenes</h2>
<span class="close" onclick="closeGallery()">&times;</span>
</div>
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: #ddd;"></i>
<p style="margin-top: 10px; color: #666;">Haz clic para subir una imagen o arrastra aquí</p>
<input type="file" id="fileInput" accept="image/*" onchange="uploadImage(this)">
</div>
<div class="gallery-grid" id="galleryGrid">
<p style="text-align: center; color: #999;">Cargando imágenes...</p>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<script>
$(document).ready(function() {
console.log('edit.php script loaded and ready');
$('#contenido').summernote({
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
]
});
// Handle form submission via Fetch API
$('#editTemplateForm').on('submit', async function(e) {
e.preventDefault(); // Prevent default form submission
const id = $('#templateId').val();
const nombre = $('#nombre').val();
const comando = $('#comando').val();
const contenido = $('#contenido').summernote('code'); // Get content from Summernote
// Clear previous alerts
$('#alert-messages').empty();
if (!nombre || !contenido) {
showAlert('El nombre y el contenido son obligatorios.', 'error');
return;
}
try {
const response = await fetch('/discord/api/templates/edit.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id, nombre, comando, contenido })
});
const result = await response.json();
if (result.success) {
showAlert('Plantilla actualizada correctamente.', 'success');
setTimeout(() => {
window.location.href = 'list.php';
}, 1500); // Redirect after a short delay
} else {
showAlert(result.error || 'Error desconocido al actualizar la plantilla.', 'error');
}
} catch (error) {
console.error('Error al enviar la solicitud:', error);
showAlert('Error de conexión al guardar la plantilla.', 'error');
}
});
});
function openGallery() {
document.getElementById('galleryModal').style.display = 'block';
loadGallery();
}
function closeGallery() {
document.getElementById('galleryModal').style.display = 'none';
}
async function loadGallery() {
try {
const response = await fetch('/gallery/api/list.php');
const data = await response.json();
const grid = document.getElementById('galleryGrid');
if (data.success && data.images.length > 0) {
grid.innerHTML = data.images.map(img => `
<div class="gallery-item" onclick="insertImage('${img.url}')">
<img src="${img.url_thumbnail}" alt="${img.nombre_original}">
</div>
`).join('');
} else {
grid.innerHTML = '<p style="text-align: center; color: #999;">No hay imágenes disponibles</p>';
}
} catch (error) {
console.error('Error cargando galería:', error);
document.getElementById('galleryGrid').innerHTML = '<p style="text-align: center; color: #c33;">Error cargando imágenes</p>';
}
}
function insertImage(url) {
const fullUrl = window.location.origin + url;
$('#contenido').summernote('insertImage', fullUrl);
closeGallery();
}
async function uploadImage(input) {
if (!input.files || !input.files[0]) return;
const formData = new FormData();
formData.append('image', input.files[0]);
try {
const response = await fetch('/gallery/api/upload.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('Imagen subida correctamente');
loadGallery();
} else {
alert('Error: ' + data.error);
}
} catch (error) {
alert('Error al subir la imagen');
console.error(error);
}
input.value = '';
}
function previewContent() {
const content = $('#contenido').summernote('code');
const win = window.open('', 'Preview', 'width=800,height=600');
win.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Vista Previa</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
background: #f8f9fa;
}
.preview-container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="preview-container">
${content}
</div>
</body>
</html>
`);
}
window.onclick = function(event) {
const modal = document.getElementById('galleryModal');
if (event.target == modal) {
closeGallery();
}
}
function showAlert(message, type) {
const alertDiv = `<div class="alert alert-${type}">${message}</div>`;
$('#alert-messages').html(alertDiv);
}
</script>
</body>
</html>

336
discord/views/templates/list.php Executable file
View File

@@ -0,0 +1,336 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Cargar configuración
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
// Verificar autenticación
$userData = JWTAuth::requireAuth();
// Verificar permiso para ver la página
if (!hasPermission('view_templates', 'discord')) {
die('No tienes permiso para ver las plantillas de Discord.');
}
// Obtener plantillas
$db = getDB();
$stmt = $db->query("
SELECT p.*, c.comando, u.username
FROM plantillas_discord p
LEFT JOIN comandos_discord c ON p.id = c.plantilla_id
LEFT JOIN usuarios u ON p.usuario_id = u.id
ORDER BY p.fecha_creacion DESC
");
$plantillas = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plantillas Discord - Sistema de Bots</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: var(--discord-color);
font-size: 24px;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-primary {
background: var(--discord-color);
color: white;
}
.btn-primary:hover {
background: var(--discord-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.templates-grid {
display: grid;
gap: 20px;
}
.template-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.template-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.template-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 15px;
}
.template-title {
font-size: 20px;
font-weight: 700;
color: #333;
margin-bottom: 5px;
}
.template-command {
display: inline-block;
background: #e3e7ff;
color: var(--discord-color);
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
font-family: monospace;
}
.template-meta {
font-size: 13px;
color: #666;
margin-bottom: 15px;
}
.template-content {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
max-height: 150px;
overflow: hidden;
position: relative;
}
.template-content::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(transparent, #f8f9fa);
}
.template-actions {
display: flex;
gap: 10px;
}
.btn-sm {
padding: 8px 16px;
font-size: 13px;
}
.empty-state {
background: white;
border-radius: 15px;
padding: 60px 40px;
text-align: center;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.empty-state i {
font-size: 64px;
color: #ddd;
margin-bottom: 20px;
}
.empty-state h2 {
color: #666;
margin-bottom: 10px;
}
.empty-state p {
color: #999;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-file-alt"></i> Plantillas Discord</h1>
<div class="header-actions">
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
<?php if (hasPermission('manage_templates', 'discord')): ?>
<a href="create.php" class="btn btn-primary">
<i class="fas fa-plus"></i> Nueva Plantilla
</a>
<?php endif; ?>
</div>
</div>
<div class="container">
<?php if (empty($plantillas)): ?>
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h2>No hay plantillas creadas</h2>
<p>Crea tu primera plantilla para empezar a enviar mensajes en Discord</p>
<?php if (hasPermission('manage_templates', 'discord')): ?>
<a href="create.php" class="btn btn-primary">
<i class="fas fa-plus"></i> Crear Primera Plantilla
</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="templates-grid">
<?php foreach ($plantillas as $plantilla): ?>
<div class="template-card">
<div class="template-header">
<div>
<div class="template-title"><?php echo htmlspecialchars($plantilla['nombre']); ?></div>
<?php if ($plantilla['comando']): ?>
<span class="template-command"><?php echo htmlspecialchars($plantilla['comando']); ?></span>
<?php endif; ?>
</div>
</div>
<div class="template-meta">
<i class="fas fa-user"></i> <?php echo htmlspecialchars($plantilla['username'] ?? 'Desconocido'); ?>
&nbsp;&nbsp;|&nbsp;&nbsp;
<i class="fas fa-clock"></i> <?php echo date('d/m/Y H:i', strtotime($plantilla['fecha_creacion'])); ?>
</div>
<div class="template-content">
<?php echo $plantilla['contenido']; ?>
</div>
<div class="template-actions">
<?php if (hasPermission('manage_templates', 'discord')): ?>
<a href="edit.php?id=<?php echo $plantilla['id']; ?>" class="btn btn-primary btn-sm">
<i class="fas fa-edit"></i> Editar
</a>
<?php endif; ?>
<?php if (hasPermission('view_templates', 'discord')): ?>
<button onclick="previewTemplate(<?php echo $plantilla['id']; ?>)" class="btn btn-secondary btn-sm">
<i class="fas fa-eye"></i> Vista Previa
</button>
<?php endif; ?>
<?php if (hasPermission('manage_templates', 'discord')): ?>
<button onclick="deleteTemplate(<?php echo $plantilla['id']; ?>, '<?php echo htmlspecialchars($plantilla['nombre'], ENT_QUOTES); ?>')" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i> Eliminar
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<script>
function previewTemplate(id) {
window.open('preview.php?id=' + id, 'preview', 'width=800,height=600');
}
async function deleteTemplate(id, nombre) {
if (!confirm(`¿Estás seguro de eliminar la plantilla "${nombre}"?`)) {
return;
}
try {
const response = await fetch('/discord/api/templates/delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id })
});
const data = await response.json();
if (data.success) {
alert('Plantilla eliminada correctamente');
location.reload();
} else {
alert('Error al eliminar: ' + data.error);
}
} catch (error) {
alert('Error de conexión');
console.error(error);
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
[29-Nov-2025 04:28:49 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
Stack trace:
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
#3 /var/www/html/bot/discord/views/templates/list.php(10): JWTAuth::requireAuth()
#4 {main}
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
[29-Nov-2025 04:37:46 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
Stack trace:
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
#4 {main}
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
[29-Nov-2025 04:38:17 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
Stack trace:
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
#4 {main}
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
[29-Nov-2025 04:40:46 America/Mexico_City] PHP Fatal error: Uncaught TypeError: Key material must be a string, resource, or OpenSSLAsymmetricKey in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php:26
Stack trace:
#0 /var/www/html/bot/shared/auth/jwt.php(61): Firebase\JWT\Key->__construct()
#1 /var/www/html/bot/shared/auth/jwt.php(137): JWTAuth::validateToken()
#2 /var/www/html/bot/shared/auth/jwt.php(163): JWTAuth::authenticate()
#3 /var/www/html/bot/discord/views/templates/list.php(15): JWTAuth::requireAuth()
#4 {main}
thrown in /var/www/html/bot/vendor/firebase/php-jwt/src/Key.php on line 26
[29-Nov-2025 04:43:07 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
[29-Nov-2025 04:44:44 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
[29-Nov-2025 05:03:31 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
[29-Nov-2025 16:24:11 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
[29-Nov-2025 16:37:32 America/Mexico_City] PHP Warning: file_put_contents(/var/www/html/bot/shared/utils/../logs/discord/templates.log): Failed to open stream: No such file or directory in /var/www/html/bot/shared/utils/helpers.php on line 40
[04-Dec-2025 14:44:31 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
[04-Dec-2025 14:45:03 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262
[04-Dec-2025 14:45:33 America/Mexico_City] PHP Warning: Undefined array key "comando" in /var/www/html/bot/discord/views/templates/list.php on line 262

View File

@@ -0,0 +1,130 @@
<?php
session_start();
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
// Verificar autenticación
$userData = JWTAuth::requireAuth();
$id = $_GET['id'] ?? null;
if (!$id) {
die('ID no proporcionado');
}
$db = getDB();
$stmt = $db->prepare("SELECT * FROM plantillas_discord WHERE id = ?");
$stmt->execute([$id]);
$plantilla = $stmt->fetch();
if (!$plantilla) {
die('Plantilla no encontrada');
}
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vista Previa: <?php echo htmlspecialchars($plantilla['nombre']); ?></title>
<style>
body {
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: #313338;
color: #dbdee1;
padding: 20px;
margin: 0;
line-height: 1.375rem;
}
.discord-message {
max-width: 800px;
margin: 0 auto;
background: #313338;
padding: 10px;
}
.message-content {
font-size: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Estilos básicos para simular Discord */
strong { font-weight: 700; }
em { font-style: italic; }
u { text-decoration: underline; }
a {
color: #00a8fc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
code {
background-color: #2b2d31;
padding: 2px;
border-radius: 3px;
font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
font-size: 0.875rem;
}
pre {
background-color: #2b2d31;
border: 1px solid #1e1f22;
border-radius: 4px;
padding: 8px;
margin: 6px 0;
max-width: 100%;
overflow-x: auto;
}
pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
font-size: 0.875rem;
}
blockquote {
margin: 0;
padding: 0 0 0 4px;
border-left: 4px solid #4e5058;
max-width: 100%;
}
img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin-top: 5px;
}
h1, h2, h3 {
margin: 8px 0;
font-weight: 700;
}
h1 { font-size: 1.5rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1rem; }
ul, ol {
margin: 8px 0;
padding-left: 24px;
}
</style>
</head>
<body>
<div class="discord-message">
<div class="message-content">
<?php echo $plantilla['contenido']; ?>
</div>
</div>
</body>
</html>

475
discord/views/welcome/config.php Executable file
View File

@@ -0,0 +1,475 @@
<?php
session_start();
// Habilitar logging para depuración
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../../shared/utils/helpers.php';
require_once __DIR__ . '/../../../shared/auth/jwt.php';
require_once __DIR__ . '/../../../shared/database/connection.php';
$userData = JWTAuth::requireAuth();
// Verificar permiso para gestionar el mensaje de bienvenida
if (!hasPermission('manage_welcome', 'discord')) {
die('No tienes permiso para gestionar la configuración del mensaje de bienvenida de Discord.');
}
$db = getDB();
// Manejar guardado
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Verificar permiso para la acción de guardar
if (!hasPermission('manage_welcome', 'discord')) {
jsonResponse(['success' => false, 'error' => 'No tienes permiso para guardar la configuración del mensaje de bienvenida.'], 403);
}
$input = json_decode(file_get_contents('php://input'), true);
try {
// Verificar si ya existe configuración (solo debe haber una por ahora, o una por servidor si escalamos)
// Por simplicidad asumimos una configuración global única (id=1) o la creamos
$canal_id = $input['canal_id'] ?? '';
$texto = $input['texto'] ?? '';
$imagen_id = !empty($input['imagen_id']) ? $input['imagen_id'] : null;
$idiomas_habilitados = json_encode($input['idiomas_habilitados'] ?? []);
$registrar = isset($input['registrar_usuario']) ? (int)$input['registrar_usuario'] : 1;
$activo = isset($input['activo']) ? (int)$input['activo'] : 1;
// Intentar actualizar primero
$stmt = $db->prepare("SELECT id FROM bienvenida_discord LIMIT 1");
$stmt->execute();
$exists = $stmt->fetchColumn();
if ($exists) {
$stmt = $db->prepare("
UPDATE bienvenida_discord
SET canal_id = ?, texto = ?, imagen_id = ?, idiomas_habilitados = ?, registrar_usuario = ?, activo = ?
WHERE id = ?
");
$stmt->execute([$canal_id, $texto, $imagen_id, $idiomas_habilitados, $registrar, $activo, $exists]);
} else {
$stmt = $db->prepare("
INSERT INTO bienvenida_discord (canal_id, texto, imagen_id, idiomas_habilitados, registrar_usuario, activo)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([$canal_id, $texto, $imagen_id, $idiomas_habilitados, $registrar, $activo]);
}
jsonResponse(['success' => true]);
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
}
exit;
}
// Obtener configuración actual
$stmt = $db->query("
SELECT b.*, g.ruta as imagen_ruta
FROM bienvenida_discord b
LEFT JOIN gallery g ON b.imagen_id = g.id
LIMIT 1
");
$config = $stmt->fetch(PDO::FETCH_ASSOC);
// Obtener canales destinatarios
$stmt = $db->query("SELECT discord_id, nombre FROM destinatarios_discord WHERE tipo = 'canal' ORDER BY nombre ASC");
$canales = $stmt->fetchAll();
// Obtener idiomas activos
$stmt = $db->query("SELECT id, codigo, nombre FROM idiomas WHERE activo = 1 ORDER BY nombre ASC");
$idiomas = $stmt->fetchAll();
// Decodificar idiomas seleccionados
$idiomasSeleccionados = [];
if ($config && $config['idiomas_habilitados']) {
$idiomasSeleccionados = json_decode($config['idiomas_habilitados'], true) ?? [];
}
?>
<!DOCTYPE html>
<html lang="<?php echo $userData->idioma ?? 'es'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bienvenida Discord - Sistema de Bots</title>
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Summernote CSS -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
:root {
--discord-color: #5865F2;
--discord-dark: #4752C4;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--discord-color) 0%, var(--discord-dark) 100%);
min-height: 100vh;
padding: 20px;
}
.header {
background: white;
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.header h1 { color: var(--discord-color); font-size: 24px; }
.container { max-width: 1000px; margin: 0 auto; }
.card {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 8px; font-weight: 600; }
.form-control { width: 100%; padding: 10px; border: 2px solid #eee; border-radius: 8px; }
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
border: none;
cursor: pointer;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary { background: var(--discord-color); color: white; }
.btn-secondary { background: #6c757d; color: white; }
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background-color: #ccc; transition: .4s; border-radius: 34px;
}
.slider:before {
position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px;
background-color: white; transition: .4s; border-radius: 50%;
}
input:checked + .slider { background-color: var(--discord-color); }
input:checked + .slider:before { transform: translateX(26px); }
/* Modal Galería */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.modal-content { background: white; margin: 5% auto; padding: 20px; width: 80%; max-width: 900px; border-radius: 15px; max-height: 80vh; overflow-y: auto; }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 20px; }
.gallery-item { border: 2px solid #eee; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.2s; position: relative; }
.gallery-item:hover { border-color: var(--discord-color); transform: translateY(-2px); }
.gallery-item img { width: 100%; height: 120px; object-fit: cover; }
.gallery-item.selected { border-color: var(--discord-color); box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.3); }
.image-preview {
width: 100%;
height: 200px;
background: #f8f9fa;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
overflow: hidden;
position: relative;
}
.image-preview img { width: 100%; height: 100%; object-fit: contain; }
.remove-image {
position: absolute; top: 10px; right: 10px;
background: rgba(255,0,0,0.8); color: white;
border: none; border-radius: 50%; width: 30px; height: 30px;
cursor: pointer; display: none;
}
</style>
</head>
<body>
<div class="header">
<h1><i class="fas fa-handshake"></i> Configuración de Bienvenida</h1>
<a href="/discord/dashboard_discord.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
<div class="container">
<div class="card">
<form id="welcomeForm">
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<div class="form-group" style="margin-bottom: 0;">
<label>Activar Bienvenida</label>
<label class="switch">
<input type="checkbox" name="activo" <?php echo ($config['activo'] ?? 1) ? 'checked' : ''; ?>>
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label>Registrar Usuario en BD</label>
<label class="switch">
<input type="checkbox" name="registrar_usuario" <?php echo ($config['registrar_usuario'] ?? 1) ? 'checked' : ''; ?>>
<span class="slider"></span>
</label>
</div>
</div>
<div class="form-group">
<label>Canal de Bienvenida</label>
<select name="canal_id" class="form-control select2" required>
<option value="">-- Seleccionar Canal --</option>
<?php foreach ($canales as $canal): ?>
<option value="<?php echo $canal['discord_id']; ?>"
<?php echo ($config['canal_id'] ?? '') == $canal['discord_id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($canal['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
<small style="color: #666;">Si no aparece, agrégalo en "Destinatarios".</small>
</div>
<div class="form-group">
<label>Idiomas Disponibles (Botones Automáticos)</label>
<select name="idiomas_habilitados[]" id="idiomas_habilitados_select" class="form-control" multiple="multiple">
<?php foreach ($idiomas as $lang): ?>
<option value="<?php echo $lang['codigo']; ?>"
<?php echo in_array($lang['codigo'], $idiomasSeleccionados) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($lang['nombre']); ?>
</option>
<?php endforeach; ?>
</select>
<small style="color: #666;">Selecciona los idiomas que se mostrarán como botones en el mensaje de bienvenida.</small>
<div style="margin-top: 10px;">
<a href="/shared/languages/manager.php" style="font-size: 12px; color: var(--discord-color); text-decoration: none;">
<i class="fas fa-cog"></i> Gestionar idiomas
</a>
</div>
</div>
<div class="form-group">
<label>Mensaje de Bienvenida</label>
<textarea id="summernote" name="texto"><?php echo htmlspecialchars($config['texto'] ?? ''); ?></textarea>
<small style="color: #666;">Puedes usar {usuario} para mencionar al nuevo miembro.</small>
</div>
<div class="form-group">
<label>Imagen Opcional</label>
<input type="hidden" id="imagen_id" name="imagen_id" value="<?php echo htmlspecialchars($config['imagen_id'] ?? ''); ?>">
<button type="button" onclick="openGallery()" class="btn btn-secondary" style="margin-bottom: 10px;">
<i class="fas fa-image"></i> Seleccionar Imagen
</button>
<div id="imagePreview" class="image-preview">
<?php if (!empty($config['imagen_id']) && !empty($config['imagen_ruta'])): ?>
<img src="/gallery/uploads/<?php echo basename($config['imagen_ruta']); ?>" alt="Imagen de bienvenida">
<button type="button" class="remove-image" style="display:block" onclick="removeImage()">×</button>
<?php else: ?>
<span style="color: #ccc;">Sin imagen seleccionada</span>
<?php endif; ?>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<?php if (hasPermission('manage_welcome', 'discord')): ?>
<button type="submit" class="btn btn-primary" style="flex: 1; justify-content: center;">
<i class="fas fa-save"></i> Guardar Configuración
</button>
<?php endif; ?>
<?php if (hasPermission('manage_welcome', 'discord')): ?>
<button type="button" onclick="sendTestMessage()" class="btn btn-success" style="flex: 1; justify-content: center; background: #28a745;">
<i class="fas fa-paper-plane"></i> Probar Mensaje
</button>
<?php endif; ?>
</div>
</form>
</div>
</div>
<!-- Modal Galería -->
<div id="galleryModal" class="modal">
<div class="modal-content">
<span style="float:right; cursor:pointer; font-size:24px;" onclick="closeGallery()">&times;</span>
<h2>Seleccionar Imagen</h2>
<div id="galleryContainer" class="gallery-grid">
<!-- Se carga vía AJAX -->
</div>
</div>
</div>
<!-- Librerías JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('#summernote').summernote({
height: 200,
toolbar: [
['style', ['bold', 'italic', 'underline', 'clear']],
['para', ['ul', 'ol']],
['insert', ['link']],
['view', ['codeview']]
]
});
$('.select2').select2();
$('#idiomas_habilitados_select').select2(); // Inicializar el select2 para idiomas
// Galería
function openGallery() {
document.getElementById('galleryModal').style.display = 'block';
loadGalleryImages();
}
function closeGallery() {
document.getElementById('galleryModal').style.display = 'none';
}
async function loadGalleryImages() {
const container = document.getElementById('galleryContainer');
container.innerHTML = '<p>Cargando...</p>';
try {
const response = await fetch('/gallery/api/list.php');
const data = await response.json();
container.innerHTML = '';
if (data.images && data.images.length > 0) {
data.images.forEach(img => {
const div = document.createElement('div');
div.className = 'gallery-item';
// La API devuelve url_thumbnail (ruta completa) y nombre_original
div.innerHTML = `<img src="${img.url_thumbnail}" alt="${img.nombre_original}">`;
// Pasamos img.nombre que es el nombre del archivo físico
div.onclick = () => selectImage(img.id, img.nombre, img.ruta); // Pasar también la ruta completa para preview
container.appendChild(div);
});
} else {
container.innerHTML = '<p>No hay imágenes en la galería.</p>';
}
} catch (error) {
console.error(error);
container.innerHTML = '<p>Error cargando imágenes.</p>';
}
}
function selectImage(id, filename, ruta_completa) {
document.getElementById('imagen_id').value = id;
const preview = document.getElementById('imagePreview');
preview.innerHTML = `
<img src="${ruta_completa}" alt="Imagen de bienvenida">
<button type="button" class="remove-image" style="display:block" onclick="removeImage()">×</button>
`;
closeGallery();
}
function removeImage() {
document.getElementById('imagen_id').value = '';
document.getElementById('imagePreview').innerHTML = `
<span style="color: #ccc;">Sin imagen seleccionada</span>
`;
}
// Guardar
document.getElementById('welcomeForm').onsubmit = async function(e) {
e.preventDefault();
const formData = new FormData(e.target);
// Recolectar datos del formulario
const activo = $('input[name="activo"]').is(':checked') ? 1 : 0;
const registrar_usuario = $('input[name="registrar_usuario"]').is(':checked') ? 1 : 0;
const canal_id = $('select[name="canal_id"]').val();
const texto = $('#summernote').summernote('code');
const imagen_id = $('#imagen_id').val();
const idiomas_habilitados = $('#idiomas_habilitados_select').val(); // Array de códigos de idioma
const data = {
activo: activo,
registrar_usuario: registrar_usuario,
canal_id: canal_id,
texto: texto,
imagen_id: imagen_id === '' ? null : imagen_id,
idiomas_habilitados: idiomas_habilitados
};
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
alert('Configuración guardada correctamente');
} else {
alert('Error: ' + result.error);
}
} catch (error) {
console.error(error);
alert('Error de conexión');
}
};
async function sendTestMessage() {
if (!confirm('¿Enviar mensaje de prueba al canal configurado?')) return;
const btn = document.querySelector('.btn-success');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enviando...';
btn.disabled = true;
try {
const response = await fetch('/discord/api/welcome/send_test.php', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert('Mensaje de prueba enviado con éxito a Discord!');
} else {
alert('Error: ' + result.error);
}
} catch (error) {
console.error(error);
alert('Error de conexión');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
window.onclick = function(event) {
if (event.target == document.getElementById('galleryModal')) {
closeGallery();
}
}
});
</script>
</body>
</html>