funcional

This commit is contained in:
lizandrogd
2026-01-12 15:54:04 -05:00
parent 4b7bcb6924
commit 50fc690b1b
17 changed files with 2089 additions and 130 deletions
+20
View File
@@ -0,0 +1,20 @@
<?php
require_once 'config/config.php';
$db = Database::getInstance();
$db->query(
"INSERT INTO message_templates (name, template_name, language_code, status) VALUES (?, ?, ?, ?)",
['Hello World Template', 'hello_world', 'en_US', 'approved']
);
echo "✅ Plantilla agregada exitosamente\n";
// Verificar
$stmt = $db->query("SELECT template_name, language_code, status FROM message_templates");
$templates = $stmt->fetchAll();
foreach ($templates as $template) {
echo "{$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
}
?>
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* Agregar plantilla hello_world que funciona
*/
require_once 'config/config.php';
header('Content-Type: text/plain; charset=utf-8');
try {
$db = Database::getInstance();
echo "=== AGREGAR PLANTILLA QUE FUNCIONA ===\n\n";
// Agregar plantilla hello_world que sabemos que funciona
$db->query(
"INSERT INTO message_templates (template_name, language_code, status, body_text, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())",
['hello_world', 'en_US', 'approved', 'Hello World! This is a test message from WhatsApp Business API.']
);
echo "✅ Agregada plantilla 'hello_world' (en_US) - FUNCIONA CORRECTAMENTE\n\n";
// Mostrar plantillas finales
echo "📋 Plantillas en la base de datos:\n";
$stmt = $db->query("SELECT id, template_name, language_code, status, body_text FROM message_templates ORDER BY template_name");
$templates = $stmt->fetchAll();
foreach ($templates as $template) {
echo "• ID: {$template['id']}\n";
echo " Nombre: {$template['template_name']}\n";
echo " Idioma: {$template['language_code']}\n";
echo " Estado: {$template['status']}\n";
echo " Texto: {$template['body_text']}\n";
echo " -----\n";
}
echo "\n🎉 Ahora puedes enviar mensajes usando la plantilla 'hello_world'\n";
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
?>
+96
View File
@@ -0,0 +1,96 @@
<?php
/**
* API - Obtener mensajes de una conversación específica
*/
require_once '../config/config.php';
// Modo debug: desactivar autenticación si existe el parámetro debug
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode) {
requireAuthentication();
}
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Headers: Content-Type');
try {
$user_id = $_GET['user_id'] ?? null;
if (!$user_id) {
http_response_code(400);
echo json_encode(['error' => 'user_id es requerido']);
exit;
}
$db = Database::getInstance();
// Obtener información del usuario
$user = $db->fetch(
"SELECT id, phone_number, name FROM users WHERE id = ?",
[$user_id]
);
if (!$user) {
http_response_code(404);
echo json_encode(['error' => 'Usuario no encontrado']);
exit;
}
// Obtener todos los mensajes de la conversación
$messages = $db->fetchAll(
"SELECT
id,
message_id,
direction,
message_type,
content,
status,
created_at
FROM conversations
WHERE user_id = ?
ORDER BY created_at ASC",
[$user_id]
);
// Formatear mensajes
$messages = array_map(function($msg) {
return [
'id' => intval($msg['id']),
'message_id' => $msg['message_id'] ?? '',
'direction' => $msg['direction'],
'message_type' => $msg['message_type'],
'content' => $msg['content'] ?? '',
'status' => $msg['status'] ?? 'sent',
'created_at' => $msg['created_at'],
'time' => date('H:i', strtotime($msg['created_at'])),
'date' => date('d/m/Y', strtotime($msg['created_at']))
];
}, $messages);
// Marcar mensajes como leídos
$db->query(
"UPDATE conversations SET status = 'read' WHERE user_id = ? AND direction = 'incoming' AND status != 'read'",
[$user_id]
);
echo json_encode([
'success' => true,
'user' => [
'id' => intval($user['id']),
'phone_number' => $user['phone_number'],
'name' => $user['name'] ?? $user['phone_number']
],
'messages' => $messages,
'total_messages' => count($messages)
]);
} catch (Exception $e) {
error_log("Error in get_conversation_detail.php: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor: ' . $e->getMessage()]);
}
?>
+95
View File
@@ -0,0 +1,95 @@
<?php
/**
* API - Obtener conversaciones agrupadas por usuario con último mensaje
*/
require_once '../config/config.php';
// Modo debug: desactivar autenticación si existe el parámetro debug
$debugMode = isset($_GET['debug']) && $_GET['debug'] === 'true';
if (!$debugMode) {
requireAuthentication();
}
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Headers: Content-Type');
try {
$db = Database::getInstance();
// Obtener conversaciones agrupadas por usuario con último mensaje
$conversations = $db->fetchAll(
"SELECT
u.id as user_id,
u.phone_number,
COALESCE(u.name, u.phone_number) as name,
c.content as last_message,
c.direction as last_direction,
c.message_type as last_message_type,
c.created_at as last_message_time,
c.status as last_message_status,
COUNT(*) as total_messages,
SUM(CASE WHEN c.direction = 'incoming' AND c.status = 'received' THEN 1 ELSE 0 END) as unread_count
FROM users u
LEFT JOIN conversations c ON u.id = c.user_id
WHERE c.id IN (
SELECT MAX(id)
FROM conversations
GROUP BY user_id
)
GROUP BY u.id, u.phone_number, u.name, c.content, c.direction, c.message_type, c.created_at, c.status
ORDER BY c.created_at DESC
LIMIT 50"
);
// Si no hay datos, retornar array vacío
if (empty($conversations)) {
$conversations = [];
}
// Formatear datos para el frontend
$conversations = array_map(function($conv) {
return [
'user_id' => intval($conv['user_id']),
'phone_number' => $conv['phone_number'],
'name' => $conv['name'],
'last_message' => $conv['last_message'] ?? '',
'last_direction' => $conv['last_direction'] ?? 'incoming',
'last_message_type' => $conv['last_message_type'] ?? 'text',
'last_message_time' => $conv['last_message_time'],
'last_message_status' => $conv['last_message_status'] ?? 'sent',
'total_messages' => intval($conv['total_messages']),
'unread_count' => intval($conv['unread_count']),
'time_ago' => timeAgo($conv['last_message_time'])
];
}, $conversations);
echo json_encode([
'success' => true,
'conversations' => $conversations,
'total' => count($conversations)
]);
} catch (Exception $e) {
error_log("Error in get_conversation_list.php: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Error interno del servidor: ' . $e->getMessage()]);
}
/**
* Calcular tiempo transcurrido
*/
function timeAgo($datetime) {
$time = time() - strtotime($datetime);
if ($time < 60) return 'Ahora';
if ($time < 3600) return floor($time/60) . 'm';
if ($time < 86400) return floor($time/3600) . 'h';
if ($time < 2592000) return floor($time/86400) . 'd';
if ($time < 31536000) return floor($time/2592000) . ' mes';
return floor($time/31536000) . ' año';
}
?>
+64 -4
View File
@@ -18,21 +18,38 @@ if (!$debugMode) {
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Manejar preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Método no permitido']);
echo json_encode(['error' => 'Método no permitido. Use POST.', 'method_received' => $_SERVER['REQUEST_METHOD']]);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true);
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
// Debug logging
if ($debugMode) {
error_log("=== SEND MESSAGE DEBUG ===");
error_log("Method: " . $_SERVER['REQUEST_METHOD']);
error_log("Content-Type: " . ($_SERVER['CONTENT_TYPE'] ?? 'not set'));
error_log("Raw input: " . $rawInput);
error_log("Parsed JSON: " . json_encode($input));
error_log("========================");
}
if (!$input) {
http_response_code(400);
echo json_encode(['error' => 'JSON inválido']);
echo json_encode(['error' => 'JSON inválido o vacío', 'raw_input' => $rawInput]);
exit;
}
@@ -241,6 +258,49 @@ try {
error_log("Response: " . json_encode($response));
error_log("======================================");
// Guardar mensaje en la base de datos
try {
// Buscar o crear usuario por teléfono
$stmt = $db->query(
"SELECT id FROM users WHERE phone_number = ? LIMIT 1",
[$recipient]
);
$user = $stmt->fetch();
$userId = null;
if ($user) {
$userId = $user['id'];
} else {
// Crear nuevo usuario
$stmt = $db->query(
"INSERT INTO users (phone_number, name, created_at) VALUES (?, ?, NOW())",
[$recipient, $recipient, date('Y-m-d H:i:s')]
);
$userId = $db->lastInsertId();
}
if ($userId) {
// Preparar datos del mensaje
$messageContent = '';
$messageType = $type;
if ($type === 'text') {
$messageContent = $input['message'];
} elseif ($type === 'template') {
$templateName = $input['template'] ?? $input['template_name'] ?? '';
$messageContent = "Plantilla: {$templateName}";
}
// Guardar en la tabla conversations
$stmt = $db->query(
"INSERT INTO conversations (user_id, content, direction, message_type, status, message_id, created_at) VALUES (?, ?, 'outgoing', ?, 'sent', ?, NOW())",
[$userId, $messageContent, $messageType, $response['messages'][0]['id'] ?? null]
);
}
} catch (Exception $e) {
error_log("Error guardando mensaje en base de datos: " . $e->getMessage());
}
echo json_encode([
'success' => true,
'message' => 'Mensaje enviado correctamente',
+300
View File
@@ -782,4 +782,304 @@ body {
box-shadow: none;
border: 1px solid #ddd;
}
}
/* =================================
CHAT STYLES
================================= */
.chat-container {
background: linear-gradient(135deg, #e5ddd5 0%, #d4c5b9 100%);
}
.conversation-row:hover {
background-color: #f8f9fa !important;
cursor: pointer;
}
.message-bubble {
position: relative;
word-wrap: break-word;
border: none;
}
.message-bubble.outgoing {
margin-left: auto;
}
.message-bubble.incoming {
margin-right: auto;
}
.message-time {
text-align: right;
margin-top: 4px;
font-size: 0.7rem;
}
.avatar-placeholder {
font-weight: bold;
font-size: 16px;
flex-shrink: 0;
}
/* Modal de chat personalizado */
#chatModal .modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
#chatModal .modal-header {
border-radius: 12px 12px 0 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
#chatModal .modal-body {
padding: 0;
}
.message-bubble.bg-success {
background: linear-gradient(135deg, #25d366, #20b358) !important;
}
.message-bubble.bg-white {
background: #ffffff !important;
border: 1px solid #e9ecef;
}
/* Mejorar responsividad de conversaciones */
@media (max-width: 768px) {
.conversation-row .btn-group .btn {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.message-bubble {
max-width: 85% !important;
}
#chatModal .modal-dialog {
max-width: 95%;
margin: 1rem auto;
}
}
/* Animaciones para mensajes */
.message-bubble {
animation: messageAppear 0.3s ease-out;
}
@keyframes messageAppear {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Estilos mejorados para lista de conversaciones */
.conversation-card {
border: none;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-radius: 20px;
overflow: hidden;
}
.conversation-card .card-header {
border: none;
background: linear-gradient(135deg, #25d366 0%, #128c7e 100%);
padding: 1.5rem;
}
.conversation-list {
max-height: 600px;
overflow-y: auto;
}
.conversation-item {
display: flex;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
background: white;
}
.conversation-item:hover {
background: #f8f9fa;
transform: translateX(5px);
}
.conversation-item:last-child {
border-bottom: none;
}
.conversation-avatar {
width: 55px;
height: 55px;
background: linear-gradient(135deg, #25d366, #128c7e);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 1.4rem;
margin-right: 15px;
box-shadow: 0 4px 12px rgba(37, 211, 102, 0.3);
position: relative;
}
.conversation-avatar.online::after {
content: '';
position: absolute;
bottom: 2px;
right: 2px;
width: 14px;
height: 14px;
background: #4ade80;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.conversation-details {
flex: 1;
min-width: 0;
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.conversation-name {
font-weight: 600;
font-size: 1.1rem;
color: #1f2937;
display: flex;
align-items: center;
gap: 8px;
}
.conversation-time {
font-size: 0.8rem;
color: #9ca3af;
white-space: nowrap;
}
.conversation-preview {
display: flex;
align-items: center;
gap: 8px;
}
.conversation-last-message {
color: #6b7280;
font-size: 0.9rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.3;
}
.conversation-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
}
.conversation-unread {
background: #25d366;
color: white;
border-radius: 50%;
padding: 4px 8px;
font-size: 0.75rem;
font-weight: bold;
min-width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.conversation-status-icon {
font-size: 0.8rem;
}
.conversation-status-icon.outgoing {
color: #25d366;
}
.conversation-status-icon.incoming {
color: #3b82f6;
}
.conversation-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s ease;
}
.conversation-item:hover .conversation-actions {
opacity: 1;
}
.conversation-actions .btn {
padding: 6px 10px;
font-size: 0.8rem;
border-radius: 8px;
border: 1px solid #e5e7eb;
background: white;
color: #6b7280;
transition: all 0.2s ease;
}
.conversation-actions .btn:hover {
background: #25d366;
color: white;
border-color: #25d366;
transform: translateY(-1px);
}
.conversation-empty {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
}
.conversation-empty .empty-icon {
font-size: 4rem;
color: #d1d5db;
margin-bottom: 20px;
}
.conversation-search {
border-radius: 25px;
border: none;
background: rgba(255, 255, 255, 0.2);
color: white;
padding: 8px 20px;
}
.conversation-search::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.conversation-search:focus {
background: rgba(255, 255, 255, 0.3);
color: white;
box-shadow: none;
border: 1px solid rgba(255, 255, 255, 0.5);
}
+480 -30
View File
@@ -409,11 +409,11 @@ class SimpleWhatsAppManager {
this.log('Cargando conversaciones');
try {
const response = await this.apiCall('get_conversations.php');
console.log('Respuesta get_conversations:', response); // Debug
const response = await this.apiCall('get_conversation_list.php');
console.log('Respuesta get_conversation_list:', response); // Debug
if (response && response.success && Array.isArray(response.data)) {
this.updateConversationsList(response.data);
if (response && response.success && Array.isArray(response.conversations)) {
this.updateConversationsList(response.conversations);
} else if (response && Array.isArray(response)) {
// Retrocompatibilidad por si la API devuelve directamente el array
this.updateConversationsList(response);
@@ -428,46 +428,201 @@ class SimpleWhatsAppManager {
}
updateConversationsList(conversations) {
const container = document.getElementById('conversations-table');
const container = document.getElementById('conversations-container');
if (!container) return;
if (conversations.length === 0) {
container.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No hay conversaciones</td></tr>';
container.innerHTML = `
<div class="conversation-empty">
<div class="empty-icon">
<i class="fas fa-comments"></i>
</div>
<h5>No hay conversaciones</h5>
<p>Los chats aparecerán aquí cuando recibas o envíes mensajes</p>
</div>
`;
return;
}
let html = '';
conversations.forEach(conv => {
const date = new Date(conv.created_at);
const formatDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' });
const timeAgo = conv.time_ago || this.formatTimeAgo(conv.last_message_time || conv.created_at);
const lastMessage = conv.last_message || conv.content || 'Sin mensajes';
const userName = conv.name || conv.phone_number;
const userInitial = userName.charAt(0).toUpperCase();
const isOnline = Math.random() > 0.6; // Simulación de estado online
// Unread badge
const unreadBadge = (conv.unread_count && conv.unread_count > 0) ?
`<div class="conversation-unread">${conv.unread_count}</div>` : '';
// Status icon
const statusIcon = conv.last_direction === 'outgoing'
? '<i class="fas fa-check-double conversation-status-icon outgoing"></i>'
: '<i class="fas fa-arrow-down conversation-status-icon incoming"></i>';
// Message type indicator
const messageTypeIcon = conv.last_message_type === 'template'
? '<i class="fas fa-file-alt text-primary" title="Plantilla"></i> '
: conv.last_message_type === 'image'
? '<i class="fas fa-image text-info" title="Imagen"></i> '
: '';
html += `
<tr>
<td>
<div>
<strong>${conv.name || conv.phone_number}</strong><br>
<small class="text-muted">${conv.phone_number}</small>
<div class="conversation-item" onclick="openChatWindow(${conv.user_id})">
<div class="conversation-avatar ${isOnline ? 'online' : ''}">
${userInitial}
</div>
<div class="conversation-details">
<div class="conversation-header">
<div class="conversation-name">
${userName}
${conv.last_message_status === 'read' ? '<i class="fas fa-check-double text-primary" title="Leído"></i>' : ''}
</div>
<div class="conversation-time">${timeAgo}</div>
</div>
</td>
<td>
<div style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">
${conv.content || 'Sin contenido'}
<div class="conversation-preview">
${statusIcon}
<div class="conversation-last-message">
${messageTypeIcon}${lastMessage}
</div>
</div>
</td>
<td><span class="badge bg-info">${conv.message_type || 'text'}</span></td>
<td><span class="badge bg-${conv.status === 'sent' ? 'success' : 'warning'}">${conv.status || 'unknown'}</span></td>
<td>${formatDate}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewConversation(${conv.user_id})">
<i class="fas fa-eye"></i> Ver
</button>
</td>
</tr>
</div>
<div class="conversation-meta">
${unreadBadge}
<div class="conversation-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openChatWindow(${conv.user_id})" title="Abrir chat">
<i class="fas fa-comments"></i>
</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); viewConversationDetails(${conv.user_id})" title="Ver detalles">
<i class="fas fa-info-circle"></i>
</button>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// Agregar funcionalidad de búsqueda
this.setupConversationSearch(conversations);
}
setupConversationSearch(conversations) {
const searchInput = document.getElementById('search-conversations');
if (!searchInput) return;
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
if (!query) {
this.updateConversationsDisplay(conversations);
return;
}
const filtered = conversations.filter(conv => {
const name = (conv.name || '').toLowerCase();
const phone = (conv.phone_number || '').toLowerCase();
const message = (conv.last_message || conv.content || '').toLowerCase();
return name.includes(query) ||
phone.includes(query) ||
message.includes(query);
});
this.updateConversationsDisplay(filtered);
});
}
updateConversationsDisplay(conversations) {
const container = document.getElementById('conversations-container');
if (!container) return;
if (conversations.length === 0) {
container.innerHTML = `
<div class="conversation-empty">
<div class="empty-icon">
<i class="fas fa-search"></i>
</div>
<h5>No se encontraron conversaciones</h5>
<p>Intenta con otros términos de búsqueda</p>
</div>
`;
return;
}
let html = '';
conversations.forEach(conv => {
const timeAgo = conv.time_ago || this.formatTimeAgo(conv.last_message_time || conv.created_at);
const lastMessage = conv.last_message || conv.content || 'Sin mensajes';
const userName = conv.name || conv.phone_number;
const userInitial = userName.charAt(0).toUpperCase();
const isOnline = Math.random() > 0.6;
const unreadBadge = (conv.unread_count && conv.unread_count > 0) ?
`<div class="conversation-unread">${conv.unread_count}</div>` : '';
const statusIcon = conv.last_direction === 'outgoing'
? '<i class="fas fa-check-double conversation-status-icon outgoing"></i>'
: '<i class="fas fa-arrow-down conversation-status-icon incoming"></i>';
const messageTypeIcon = conv.last_message_type === 'template'
? '<i class="fas fa-file-alt text-primary" title="Plantilla"></i> '
: conv.last_message_type === 'image'
? '<i class="fas fa-image text-info" title="Imagen"></i> '
: '';
html += `
<div class="conversation-item" onclick="openChatWindow(${conv.user_id})">
<div class="conversation-avatar ${isOnline ? 'online' : ''}">
${userInitial}
</div>
<div class="conversation-details">
<div class="conversation-header">
<div class="conversation-name">
${userName}
${conv.last_message_status === 'read' ? '<i class="fas fa-check-double text-primary" title="Leído"></i>' : ''}
</div>
<div class="conversation-time">${timeAgo}</div>
</div>
<div class="conversation-preview">
${statusIcon}
<div class="conversation-last-message">
${messageTypeIcon}${lastMessage}
</div>
</div>
</div>
<div class="conversation-meta">
${unreadBadge}
<div class="conversation-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openChatWindow(${conv.user_id})" title="Abrir chat">
<i class="fas fa-comments"></i>
</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); viewConversationDetails(${conv.user_id})" title="Ver detalles">
<i class="fas fa-info-circle"></i>
</button>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
formatTimeAgo(datetime) {
if (!datetime) return '';
const now = new Date();
const date = new Date(datetime);
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Ahora';
if (diff < 3600) return `${Math.floor(diff/60)}m`;
if (diff < 86400) return `${Math.floor(diff/3600)}h`;
if (diff < 2592000) return `${Math.floor(diff/86400)}d`;
return date.toLocaleDateString();
}
async loadSettings() {
@@ -918,10 +1073,305 @@ window.debugAPI = async function (endpoint) {
}
};
// Función para ver conversación específica
// Función para abrir ventana de chat
window.openChatWindow = function (userId) {
console.log('Abriendo chat del usuario:', userId);
// Abrir nueva ventana de chat
const chatUrl = `chat_window.php?user_id=${userId}&debug=true`;
const windowFeatures = 'width=1000,height=700,resizable=yes,scrollbars=yes,status=yes,toolbar=no,menubar=no,location=no';
const chatWindow = window.open(chatUrl, `chat_${userId}`, windowFeatures);
if (chatWindow) {
chatWindow.focus();
} else {
alert('No se pudo abrir la ventana de chat. Verifica que tu navegador permita ventanas emergentes.');
}
};
// Función para mostrar modal de chat
function showChatModal(user, messages) {
// Escapar datos del usuario para evitar XSS
const userName = escapeHtml(user.name || 'Usuario');
const userPhone = escapeHtml(user.phone_number || '');
const modalHtml = `
<div class="modal fade" id="chatModal" tabindex="-1" aria-labelledby="chatModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="chatModalLabel">
<i class="fab fa-whatsapp"></i> ${userName}
<small class="opacity-75">(${userPhone})</small>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div id="chat-messages" class="chat-container" style="height: 400px; overflow-y: auto; padding: 15px; background-color: #e5ddd5;">
${generateChatMessages(messages)}
</div>
<div class="border-top p-3">
<div class="row">
<div class="col">
<div class="input-group">
<select id="messageType" class="form-select" style="max-width: 120px;">
<option value="text">Texto</option>
<option value="template">Plantilla</option>
</select>
<input type="text" id="messageInput" class="form-control" placeholder="Escribe tu mensaje...">
<button class="btn btn-success" onclick="sendChatMessage(${user.id})">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<div id="templateSelector" class="mt-2" style="display: none;">
<select id="templateSelect" class="form-select">
<option value="">Selecciona una plantilla...</option>
<option value="hello_world">hello_world</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
`;
// Remover modal anterior si existe
const existingModal = document.getElementById('chatModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Event listener para cambio de tipo de mensaje
document.getElementById('messageType').addEventListener('change', function() {
const templateSelector = document.getElementById('templateSelector');
const messageInput = document.getElementById('messageInput');
if (this.value === 'template') {
templateSelector.style.display = 'block';
messageInput.placeholder = 'Parámetros de la plantilla (opcional)';
} else {
templateSelector.style.display = 'none';
messageInput.placeholder = 'Escribe tu mensaje...';
}
});
// Mostrar modal
const modal = new bootstrap.Modal(document.getElementById('chatModal'));
modal.show();
// Scroll al final
setTimeout(() => {
const chatContainer = document.getElementById('chat-messages');
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 200);
}
// Generar HTML de mensajes del chat
function generateChatMessages(messages) {
if (!messages || messages.length === 0) {
return '<div class="text-center text-muted py-4"><i class="fas fa-comments fa-3x"></i><br><br>No hay mensajes en esta conversación</div>';
}
let html = '';
messages.forEach(msg => {
const isOutgoing = msg.direction === 'outgoing';
const messageClass = isOutgoing ? 'outgoing' : 'incoming';
const alignClass = isOutgoing ? 'justify-content-end' : 'justify-content-start';
const bgClass = isOutgoing ? 'bg-success text-white' : 'bg-white';
// Escapar el contenido del mensaje
const messageContent = escapeHtml(msg.content) || '<em>Sin contenido</em>';
const messageTime = escapeHtml(msg.time || '');
html += `
<div class="d-flex ${alignClass} mb-2">
<div class="message-bubble ${bgClass} rounded-3 p-2 shadow-sm" style="max-width: 70%;">
<div class="message-content">
${messageContent}
</div>
<div class="message-time text-${isOutgoing ? 'light' : 'muted'}" style="font-size: 0.75rem;">
${messageTime} ${isOutgoing ? '✓✓' : ''}
</div>
</div>
</div>
`;
});
return html;
}
// Función para escapar HTML y prevenir XSS
function escapeHtml(text) {
if (!text) return '';
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.toString().replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Función para enviar mensaje desde el chat
window.sendChatMessage = async function (userId) {
const messageType = document.getElementById('messageType').value;
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message && messageType === 'text') {
alert('Por favor escribe un mensaje');
return;
}
if (messageType === 'template') {
const templateSelect = document.getElementById('templateSelect');
const template = templateSelect.value;
if (!template) {
alert('Por favor selecciona una plantilla');
return;
}
await sendTemplateMessage(userId, template, message);
} else {
await sendTextMessage(userId, message);
}
messageInput.value = '';
};
// Función para enviar mensaje de texto
async function sendTextMessage(userId, message) {
try {
const manager = window.whatsappManager;
const usersResponse = await manager.apiCall('get_users.php?debug=true');
// Manejar diferentes formatos de respuesta
let users = [];
if (Array.isArray(usersResponse)) {
users = usersResponse;
} else if (usersResponse && usersResponse.data && Array.isArray(usersResponse.data)) {
users = usersResponse.data;
} else if (usersResponse && usersResponse.users && Array.isArray(usersResponse.users)) {
users = usersResponse.users;
} else {
console.error('Formato de respuesta de usuarios inesperado:', usersResponse);
throw new Error('Error obteniendo lista de usuarios');
}
const user = users.find(u => u.id == userId);
if (!user) {
throw new Error('Usuario no encontrado');
}
const response = await manager.apiCall('send_message.php', {
method: 'POST',
body: {
recipient: user.phone_number,
type: 'text',
message: message
}
});
if (response && response.success) {
// Refrescar chat
openChatWindow(userId);
showAlert('Mensaje enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando mensaje');
}
} catch (error) {
console.error('Error enviando mensaje:', error);
showAlert('Error enviando mensaje: ' + error.message, 'danger');
}
}
// Función para enviar mensaje de plantilla
async function sendTemplateMessage(userId, template, parameters) {
try {
const manager = window.whatsappManager;
const usersResponse = await manager.apiCall('get_users.php?debug=true');
// Manejar diferentes formatos de respuesta
let users = [];
if (Array.isArray(usersResponse)) {
users = usersResponse;
} else if (usersResponse && usersResponse.data && Array.isArray(usersResponse.data)) {
users = usersResponse.data;
} else if (usersResponse && usersResponse.users && Array.isArray(usersResponse.users)) {
users = usersResponse.users;
} else {
console.error('Formato de respuesta de usuarios inesperado:', usersResponse);
throw new Error('Error obteniendo lista de usuarios');
}
const user = users.find(u => u.id == userId);
if (!user) {
throw new Error('Usuario no encontrado');
}
const response = await manager.apiCall('send_message.php', {
method: 'POST',
body: {
recipient: user.phone_number,
type: 'template',
template: template,
language: 'en_US',
parameters: parameters ? parameters.split(',') : []
}
});
if (response && response.success) {
// Refrescar chat
openChatWindow(userId);
showAlert('Mensaje de plantilla enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando plantilla');
}
} catch (error) {
console.error('Error enviando plantilla:', error);
showAlert('Error enviando plantilla: ' + error.message, 'danger');
}
}
// Función para ver detalles de conversación
window.viewConversationDetails = function (userId) {
console.log('Viendo detalles de conversación del usuario:', userId);
openChatWindow(userId);
};
// Función para mostrar alertas
function showAlert(message, type = 'info') {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show position-fixed" style="top: 20px; right: 20px; z-index: 9999;" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', alertHtml);
// Auto-dismiss después de 5 segundos
setTimeout(() => {
const alert = document.querySelector('.alert');
if (alert) {
alert.remove();
}
}, 5000);
}
// Función para ver conversación específica (mantener compatibilidad)
window.viewConversation = function (userId) {
console.log('Viendo conversación del usuario:', userId);
alert(`Ver conversación del usuario ${userId} - Función en desarrollo`);
openChatWindow(userId);
};
// Función para editar usuario
+575
View File
@@ -0,0 +1,575 @@
<?php
session_start();
// Verificar autenticación
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
// Permitir acceso en modo debug
if (!isset($_GET['debug']) || $_GET['debug'] !== 'true') {
header('Location: login.php');
exit;
}
}
$user_id = $_GET['user_id'] ?? '';
if (empty($user_id)) {
die('ID de usuario requerido');
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat WhatsApp - Usuario</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="assets/css/styles.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.chat-container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin: 20px auto;
max-width: 900px;
height: calc(100vh - 40px);
display: flex;
flex-direction: column;
}
.chat-header {
background: linear-gradient(135deg, #25D366 0%, #128C7E 100%);
color: white;
padding: 20px;
border-radius: 15px 15px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f8f9fa;
}
.chat-input {
padding: 20px;
background: white;
border-radius: 0 0 15px 15px;
border-top: 1px solid #eee;
}
.message-bubble {
margin: 10px 0;
padding: 12px 16px;
border-radius: 18px;
max-width: 70%;
word-wrap: break-word;
}
.message-outgoing {
background: #DCF8C6;
margin-left: auto;
border-bottom-right-radius: 4px;
}
.message-incoming {
background: white;
margin-right: auto;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.message-time {
font-size: 0.75rem;
color: #999;
margin-top: 5px;
text-align: right;
}
.message-incoming .message-time {
text-align: left;
}
.typing-indicator {
display: none;
padding: 10px;
font-style: italic;
color: #666;
}
.input-group {
border-radius: 25px;
overflow: hidden;
}
.form-control {
border: none;
padding: 12px 20px;
}
.btn-send {
background: #25D366;
border: none;
color: white;
padding: 12px 20px;
border-radius: 0 25px 25px 0;
}
.btn-send:hover {
background: #128C7E;
color: white;
}
.user-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
background: #25D366;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 1.2rem;
}
.status-indicator {
font-size: 0.8rem;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="chat-container">
<!-- Header del chat -->
<div class="chat-header">
<div class="d-flex align-items-center">
<div class="user-avatar me-3" id="userAvatar">
<i class="fas fa-user"></i>
</div>
<div>
<h5 class="mb-0" id="userName">Cargando...</h5>
<small class="status-indicator" id="userStatus">
<i class="fas fa-circle text-success"></i> En línea
</small>
</div>
</div>
<div class="d-flex align-items-center">
<button class="btn btn-outline-light btn-sm me-2" onclick="refreshChat()">
<i class="fas fa-sync-alt"></i>
</button>
<button class="btn btn-outline-light btn-sm" onclick="window.close()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Mensajes del chat -->
<div class="chat-messages" id="chatMessages">
<div class="d-flex justify-content-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando mensajes...</span>
</div>
</div>
</div>
<!-- Indicador de escritura -->
<div class="typing-indicator" id="typingIndicator">
<i class="fas fa-circle"></i>
<i class="fas fa-circle"></i>
<i class="fas fa-circle"></i>
El usuario está escribiendo...
</div>
<!-- Input de mensaje -->
<div class="chat-input">
<div class="row mb-3">
<div class="col-md-4">
<select class="form-select" id="messageType">
<option value="text">Mensaje de texto</option>
<option value="template">Plantilla</option>
</select>
</div>
<div class="col-md-4" id="templateSelectContainer" style="display: none;">
<select class="form-select" id="templateSelect">
<option value="">Seleccionar plantilla...</option>
</select>
</div>
</div>
<div class="input-group">
<input type="text" class="form-control" id="messageInput"
placeholder="Escribe un mensaje..."
onkeypress="handleEnterKey(event)">
<button class="btn btn-send" onclick="sendMessage()">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Variables globales
const userId = <?php echo json_encode($user_id); ?>;
let currentUser = null;
let whatsappManager = null;
// Manager para API calls
class WhatsAppManager {
constructor() {
this.isInitialized = true;
}
async apiCall(endpoint, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
};
// Configurar método y cuerpo si es POST
if (options.body) {
defaultOptions.method = 'POST';
defaultOptions.body = JSON.stringify(options.body);
}
// Combinar opciones correctamente
const finalOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...(options.headers || {})
}
};
// Si había un body en options, asegurar que se serializa correctamente
if (options.body && typeof options.body === 'object') {
finalOptions.body = JSON.stringify(options.body);
}
const url = endpoint.startsWith('http') ? endpoint : `api/${endpoint}`;
const response = await fetch(url, finalOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
}
// Inicializar manager
whatsappManager = new WhatsAppManager();
// Función para escapar HTML
function escapeHtml(text) {
if (!text) return '';
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.toString().replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Cargar datos del usuario y mensajes
async function loadUserData() {
try {
const response = await whatsappManager.apiCall(`get_conversation_detail.php?user_id=${userId}`);
if (response && response.success) {
currentUser = response.user;
// Actualizar header
document.getElementById('userName').textContent = currentUser.name || 'Usuario';
document.getElementById('userAvatar').innerHTML =
`<span>${(currentUser.name || 'U').charAt(0).toUpperCase()}</span>`;
// Cargar mensajes
displayMessages(response.messages);
// Cargar plantillas
loadTemplates();
} else {
showError('Error cargando datos del usuario');
}
} catch (error) {
console.error('Error:', error);
showError('Error cargando conversación: ' + error.message);
}
}
// Mostrar mensajes
function displayMessages(messages) {
const chatMessages = document.getElementById('chatMessages');
if (!messages || messages.length === 0) {
chatMessages.innerHTML = `
<div class="text-center text-muted py-5">
<i class="fas fa-comments fa-3x mb-3"></i>
<br>No hay mensajes en esta conversación
<br><small>Envía el primer mensaje para comenzar</small>
</div>
`;
return;
}
let html = '';
messages.forEach(msg => {
const isOutgoing = msg.direction === 'outgoing';
const messageClass = isOutgoing ? 'message-outgoing' : 'message-incoming';
const messageContent = escapeHtml(msg.content) || '<em>Sin contenido</em>';
const messageTime = escapeHtml(msg.time || '');
html += `
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
<div class="message-bubble ${messageClass}">
<div class="message-content">${messageContent}</div>
<div class="message-time">
${messageTime} ${isOutgoing ? '<i class="fas fa-check-double text-primary"></i>' : ''}
</div>
</div>
</div>
`;
});
chatMessages.innerHTML = html;
scrollToBottom();
}
// Cargar plantillas
async function loadTemplates() {
try {
const response = await whatsappManager.apiCall('check_templates.php');
const templateSelect = document.getElementById('templateSelect');
if (response && response.templates) {
templateSelect.innerHTML = '<option value="">Seleccionar plantilla...</option>';
response.templates.forEach(template => {
templateSelect.innerHTML += `<option value="${template.name}">${template.display_name || template.name}</option>`;
});
}
} catch (error) {
console.error('Error cargando plantillas:', error);
}
}
// Enviar mensaje
async function sendMessage() {
const messageType = document.getElementById('messageType').value;
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message && messageType === 'text') {
alert('Por favor escribe un mensaje');
return;
}
if (messageType === 'template') {
const templateSelect = document.getElementById('templateSelect');
const template = templateSelect.value;
if (!template) {
alert('Por favor selecciona una plantilla');
return;
}
await sendTemplateMessage(template, message);
} else {
await sendTextMessage(message);
}
messageInput.value = '';
}
// Enviar mensaje de texto
async function sendTextMessage(message) {
try {
showTyping();
console.log('Enviando mensaje de texto:', {
message,
user: currentUser
});
const requestBody = {
recipient: currentUser.phone_number,
type: 'text',
message: message
};
console.log('Request body:', requestBody);
const response = await whatsappManager.apiCall('send_message.php', {
body: requestBody
});
console.log('Response:', response);
hideTyping();
if (response && response.success) {
// Agregar mensaje a la vista inmediatamente
addMessageToView(message, 'outgoing');
showAlert('Mensaje enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando mensaje');
}
} catch (error) {
hideTyping();
console.error('Error enviando mensaje:', error);
showAlert('Error enviando mensaje: ' + error.message, 'danger');
}
}
// Enviar mensaje de plantilla
async function sendTemplateMessage(template, parameters) {
try {
showTyping();
console.log('Enviando plantilla:', {
template,
parameters,
user: currentUser
});
const requestBody = {
recipient: currentUser.phone_number,
type: 'template',
template: template,
language: 'en_US',
parameters: parameters ? parameters.split(',') : []
};
console.log('Request body:', requestBody);
const response = await whatsappManager.apiCall('send_message.php', {
body: requestBody
});
console.log('Response:', response);
hideTyping();
if (response && response.success) {
addMessageToView(`Plantilla: ${template}`, 'outgoing');
showAlert('Mensaje de plantilla enviado correctamente', 'success');
} else {
throw new Error(response.error || 'Error enviando plantilla');
}
} catch (error) {
hideTyping();
console.error('Error enviando plantilla:', error);
showAlert('Error enviando plantilla: ' + error.message, 'danger');
}
}
// Agregar mensaje a la vista
function addMessageToView(message, direction) {
const chatMessages = document.getElementById('chatMessages');
const isOutgoing = direction === 'outgoing';
const messageClass = isOutgoing ? 'message-outgoing' : 'message-incoming';
const now = new Date().toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
const messageHtml = `
<div class="d-flex ${isOutgoing ? 'justify-content-end' : 'justify-content-start'}">
<div class="message-bubble ${messageClass}">
<div class="message-content">${escapeHtml(message)}</div>
<div class="message-time">
${now} ${isOutgoing ? '<i class="fas fa-check text-muted"></i>' : ''}
</div>
</div>
</div>
`;
chatMessages.insertAdjacentHTML('beforeend', messageHtml);
scrollToBottom();
}
// Mostrar indicador de escritura
function showTyping() {
document.getElementById('typingIndicator').style.display = 'block';
}
function hideTyping() {
document.getElementById('typingIndicator').style.display = 'none';
}
// Scroll al final
function scrollToBottom() {
const chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Refrescar chat
async function refreshChat() {
await loadUserData();
}
// Manejar Enter para enviar
function handleEnterKey(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
// Mostrar/ocultar selector de plantillas
document.getElementById('messageType').addEventListener('change', function() {
const templateContainer = document.getElementById('templateSelectContainer');
if (this.value === 'template') {
templateContainer.style.display = 'block';
} else {
templateContainer.style.display = 'none';
}
});
// Mostrar alertas
function showAlert(message, type = 'info') {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show position-fixed"
style="top: 20px; right: 20px; z-index: 9999;" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', alertHtml);
setTimeout(() => {
const alert = document.querySelector('.alert');
if (alert) {
alert.remove();
}
}, 5000);
}
function showError(message) {
showAlert(message, 'danger');
}
// Inicializar cuando la página se carga
document.addEventListener('DOMContentLoaded', function() {
loadUserData();
// Auto-refresh cada 30 segundos para nuevos mensajes
setInterval(refreshChat, 30000);
});
</script>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
<?php
/**
* Ver estructura de la tabla message_templates
*/
require_once 'config/config.php';
header('Content-Type: text/plain; charset=utf-8');
try {
$db = Database::getInstance();
echo "=== ESTRUCTURA DE TABLA message_templates ===\n\n";
// Describir la tabla
$stmt = $db->query("DESCRIBE message_templates");
$columns = $stmt->fetchAll();
echo "Columnas disponibles:\n";
foreach ($columns as $column) {
echo "{$column['Field']} ({$column['Type']}) - {$column['Null']} - {$column['Default']}\n";
}
echo "\n=== INTENTAR INSERCIÓN SIMPLE ===\n";
// Intentar inserción con campos mínimos
$stmt = $db->query(
"INSERT INTO message_templates (template_name, language_code, status) VALUES (?, ?, ?)",
['hello_world', 'en_US', 'approved']
);
if ($stmt) {
echo "✅ Plantilla 'hello_world' agregada exitosamente\n";
}
// Verificar que se agregó
echo "\n📋 Plantillas en la BD:\n";
$stmt = $db->query("SELECT * FROM message_templates");
$templates = $stmt->fetchAll();
foreach ($templates as $template) {
echo "{$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
}
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Eliminar plantillas que no funcionan de la base de datos
*/
require_once 'config/config.php';
header('Content-Type: text/plain; charset=utf-8');
try {
$db = Database::getInstance();
echo "=== ELIMINAR PLANTILLAS QUE NO FUNCIONAN ===\n\n";
// Mostrar plantillas actuales
echo "📋 Plantillas actuales en la BD:\n";
$stmt = $db->query("SELECT id, template_name, language_code, status FROM message_templates ORDER BY template_name");
$templates = $stmt->fetchAll();
foreach ($templates as $template) {
echo "• ID: {$template['id']} - {$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
}
echo "\n🗑️ Eliminando plantillas que no funcionan...\n";
// Eliminar plantillas que no funcionan
$templatesToDelete = [
'welcome_user',
'jaspers_market_plain_text_v1'
];
foreach ($templatesToDelete as $templateName) {
$stmt = $db->query(
"DELETE FROM message_templates WHERE template_name = ?",
[$templateName]
);
$affectedRows = $stmt->rowCount();
echo "• Eliminada '{$templateName}': {$affectedRows} registro(s) afectado(s)\n";
}
echo "\n✅ Plantillas restantes en la BD:\n";
$stmt = $db->query("SELECT id, template_name, language_code, status FROM message_templates ORDER BY template_name");
$remainingTemplates = $stmt->fetchAll();
if (empty($remainingTemplates)) {
echo "❌ No hay plantillas restantes\n";
echo "\n💡 Sugerencia: Agrega la plantilla 'hello_world' que funciona:\n";
// Agregar plantilla hello_world que sabemos que funciona
$db->query(
"INSERT INTO message_templates (template_name, language_code, status, body_text, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())",
['hello_world', 'en_US', 'approved', 'Hello World! This is a test message.']
);
echo "✅ Agregada plantilla 'hello_world' (en_US) que funciona correctamente\n";
// Mostrar plantillas finales
$stmt = $db->query("SELECT id, template_name, language_code, status FROM message_templates ORDER BY template_name");
$finalTemplates = $stmt->fetchAll();
echo "\n📋 Plantillas finales:\n";
foreach ($finalTemplates as $template) {
echo "• ID: {$template['id']} - {$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
}
} else {
foreach ($remainingTemplates as $template) {
echo "• ID: {$template['id']} - {$template['template_name']} ({$template['language_code']}) - {$template['status']}\n";
}
}
echo "\n🎉 Limpieza completada exitosamente\n";
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
?>
+3
View File
@@ -637,8 +637,11 @@ function saveWhatsAppConfigToDB($whatsappConfig) {
*/
function getWhatsAppConfigFromDB() {
return [
// Mapear a nombres esperados por el código
'token' => getConfigFromDB('whatsapp_token', ''),
'whatsapp_token' => getConfigFromDB('whatsapp_token', ''),
'phone_number_id' => getConfigFromDB('whatsapp_phone_number_id', ''),
'api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
'whatsapp_api_url' => getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'),
'webhook_verify_token' => getConfigFromDB('webhook_verify_token', ''),
'business_name' => getConfigFromDB('business_name', ''),
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* Debug de plantillas - Ver qué plantillas hay y con qué códigos de idioma
*/
require_once 'config/config.php';
header('Content-Type: text/plain; charset=utf-8');
try {
$db = Database::getInstance();
echo "=== PLANTILLAS EN BASE DE DATOS ===\n\n";
$stmt = $db->query("SELECT template_name, language_code, status FROM message_templates ORDER BY template_name, language_code");
$templates = $stmt->fetchAll();
if (empty($templates)) {
echo "❌ No hay plantillas en la base de datos\n";
} else {
echo "📋 Plantillas encontradas:\n\n";
foreach ($templates as $template) {
echo "• Nombre: {$template['template_name']}\n";
echo " Idioma: {$template['language_code']}\n";
echo " Estado: {$template['status']}\n";
echo " -----\n";
}
}
echo "\n=== CONFIGURACIÓN ACTUAL ===\n";
$config = getWhatsAppConfigFromDB();
echo "Token presente: " . (empty($config['token']) ? '❌ NO' : '✅ SÍ (' . strlen($config['token']) . ' chars)') . "\n";
echo "Phone ID: " . ($config['phone_number_id'] ?? 'No configurado') . "\n";
echo "API URL: " . ($config['api_url'] ?? 'No configurada') . "\n";
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage() . "\n";
}
?>
+20 -22
View File
@@ -183,30 +183,28 @@ try {
<!-- Conversations Tab -->
<div id="conversations" class="tab-content">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-comments"></i> Conversaciones</h5>
<div>
<input type="text" class="form-control" placeholder="Buscar conversaciones..." id="search-conversations">
<div class="card conversation-card">
<div class="card-header bg-gradient-primary text-white">
<div class="row align-items-center">
<div class="col">
<h5 class="mb-0"><i class="fas fa-comments"></i> Conversaciones</h5>
<small class="text-white-50">Administra tus chats de WhatsApp</small>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-light border-0">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control border-0"
placeholder="Buscar conversaciones..."
id="search-conversations">
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Usuario</th>
<th>Último Mensaje</th>
<th>Tipo</th>
<th>Estado</th>
<th>Fecha</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="conversations-table">
<!-- Contenido dinámico -->
</tbody>
</table>
<div class="card-body p-0">
<div class="conversation-list" id="conversations-container">
<!-- Contenido dinámico -->
</div>
</div>
</div>
+2
View File
@@ -2,3 +2,5 @@
[2026-01-12 13:26:51] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
[2026-01-12 13:28:56] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
[2026-01-12 14:07:16] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
[2026-01-12 14:24:57] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
[2026-01-12 14:25:28] [INFO] Configuración de WhatsApp actualizada {"configured_fields":{"token":true,"phone_id":true,"webhook_token":true},"user":"debug_user"}
+121 -74
View File
@@ -1,26 +1,33 @@
<?php
/**
* Servicio de WhatsApp - Envío de mensajes
* Fecha: 13 de noviembre de 2025
*/
class WhatsAppService {
class WhatsAppService
{
private $token;
private $phoneNumberId;
private $apiUrl;
private $db;
public function __construct() {
$this->token = WHATSAPP_TOKEN;
$this->phoneNumberId = WHATSAPP_PHONE_NUMBER_ID;
$this->apiUrl = WHATSAPP_API_URL;
public function __construct()
{
// Obtener configuración desde base de datos
$config = getWhatsAppConfigFromDB();
$this->token = $config['token']; // Usar token de BD
$this->phoneNumberId = $config['phone_number_id']; // Usar phone_number_id de BD
$this->apiUrl = $config['api_url'] ?: 'https://graph.facebook.com/v22.0/'; // Usar api_url de BD
$this->db = Database::getInstance();
}
/**
* Enviar mensaje de texto
*/
public function sendTextMessage($to, $message) {
public function sendTextMessage($to, $message)
{
$data = [
'messaging_product' => 'whatsapp',
'to' => $this->formatPhoneNumber($to),
@@ -29,44 +36,62 @@ class WhatsAppService {
'body' => $message
]
];
return $this->sendMessage($data);
}
/**
* Enviar mensaje con template
*/
public function sendTemplateMessage($to, $templateName, $language = 'es', $parameters = []) {
public function sendTemplateMessage(
$to,
$templateName,
$language = 'es',
$bodyParameters = [],
$headerParameters = []
) {
$components = [];
if (!empty($headerParameters)) {
$components[] = [
'type' => 'header',
'parameters' => $headerParameters
];
}
if (!empty($bodyParameters)) {
$components[] = [
'type' => 'body',
'parameters' => $bodyParameters
];
}
$template = [
'name' => $templateName,
'language' => [
'code' => $language
]
];
if (!empty($parameters)) {
$template['components'] = [
[
'type' => 'body',
'parameters' => $parameters
]
];
if (!empty($components)) {
$template['components'] = $components;
}
$data = [
'messaging_product' => 'whatsapp',
'to' => $this->formatPhoneNumber($to),
'type' => 'template',
'template' => $template
];
return $this->sendMessage($data);
}
/**
* Enviar mensaje con botones interactivos
*/
public function sendInteractiveMessage($to, $bodyText, $buttons, $header = null, $footer = null) {
public function sendInteractiveMessage($to, $bodyText, $buttons, $header = null, $footer = null)
{
$interactive = [
'type' => 'button',
'body' => [
@@ -76,20 +101,20 @@ class WhatsAppService {
'buttons' => $buttons
]
];
if ($header) {
$interactive['header'] = [
'type' => 'text',
'text' => $header
];
}
if ($footer) {
$interactive['footer'] = [
'text' => $footer
];
}
$data = [
'messaging_product' => 'whatsapp',
'recipient_type' => 'individual',
@@ -97,14 +122,15 @@ class WhatsAppService {
'type' => 'interactive',
'interactive' => $interactive
];
return $this->sendMessage($data);
}
/**
* Enviar mensaje con lista
*/
public function sendListMessage($to, $bodyText, $buttonText, $sections, $header = null, $footer = null) {
public function sendListMessage($to, $bodyText, $buttonText, $sections, $header = null, $footer = null)
{
$interactive = [
'type' => 'list',
'body' => [
@@ -115,20 +141,20 @@ class WhatsAppService {
'sections' => $sections
]
];
if ($header) {
$interactive['header'] = [
'type' => 'text',
'text' => $header
];
}
if ($footer) {
$interactive['footer'] = [
'text' => $footer
];
}
$data = [
'messaging_product' => 'whatsapp',
'recipient_type' => 'individual',
@@ -136,51 +162,61 @@ class WhatsAppService {
'type' => 'interactive',
'interactive' => $interactive
];
return $this->sendMessage($data);
}
/**
* Marcar mensaje como leído
*/
public function markAsRead($messageId) {
public function markAsRead($messageId)
{
$data = [
'messaging_product' => 'whatsapp',
'status' => 'read',
'message_id' => $messageId
];
$url = $this->apiUrl . $this->phoneNumberId . '/messages';
return $this->makeRequest('POST', $url, $data);
}
/**
* Enviar mensaje principal
*/
private function sendMessage($data) {
$url = $this->apiUrl . $this->phoneNumberId . '/messages';
$response = $this->makeRequest('POST', $url, $data);
private function sendMessage($data)
{
// Construir URL correctamente
$url = rtrim($this->apiUrl, '/') . '/' . $this->phoneNumberId . '/messages';
// Debug log
error_log("WhatsApp API URL: " . $url);
error_log("WhatsApp Token length: " . strlen($this->token));
error_log("WhatsApp Data: " . json_encode($data));
$response = $this->makeRequest('POST', $url, $data);
// Guardar mensaje enviado en la base de datos
if ($response && isset($response['messages'][0]['id'])) {
$this->saveOutgoingMessage($data, $response);
}
return $response;
}
/**
* Realizar petición HTTP
*/
private function makeRequest($method, $url, $data = null) {
private function makeRequest($method, $url, $data = null)
{
$ch = curl_init();
$headers = [
'Authorization: Bearer ' . $this->token,
'Content-Type: application/json',
'User-Agent: WhatsApp-Bot/1.0'
];
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
@@ -192,57 +228,66 @@ class WhatsAppService {
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3
]);
if ($method === 'POST' && $data) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("cURL Error: " . $error);
throw new Exception("Error de comunicación con WhatsApp: " . $error);
}
$decoded = json_decode($response, true);
if ($httpCode >= 400) {
$errorMsg = isset($decoded['error']['message']) ? $decoded['error']['message'] : 'Error desconocido';
error_log("WhatsApp API Error: " . $response);
// Debug detallado
error_log("WhatsApp API Error Details:");
error_log("- URL: " . $url);
error_log("- HTTP Code: " . $httpCode);
error_log("- Response: " . $response);
error_log("- Token (first 20 chars): " . substr($this->token, 0, 20) . "...");
throw new Exception("Error de WhatsApp API: " . $errorMsg);
}
return $decoded;
}
/**
* Formatear número de teléfono
*/
private function formatPhoneNumber($phone) {
private function formatPhoneNumber($phone)
{
// Remover caracteres especiales
$phone = preg_replace('/[^0-9]/', '', $phone);
// Si empieza con +57 (Colombia), mantenerlo
if (substr($phone, 0, 2) === '57' && strlen($phone) >= 12) {
return $phone;
}
// Si es número colombiano sin código de país
if (strlen($phone) === 10 && substr($phone, 0, 1) === '3') {
return '57' . $phone;
}
return $phone;
}
/**
* Guardar mensaje enviado en BD
*/
private function saveOutgoingMessage($data, $response) {
private function saveOutgoingMessage($data, $response)
{
try {
$user = $this->getUserByPhone($data['to']);
if ($user) {
@@ -255,18 +300,19 @@ class WhatsAppService {
'status' => 'sent',
'created_at' => date('Y-m-d H:i:s')
];
$this->db->insert('conversations', $messageData);
}
} catch (Exception $e) {
error_log("Error saving outgoing message: " . $e->getMessage());
}
}
/**
* Extraer contenido del mensaje para guardar
*/
private function extractMessageContent($data) {
private function extractMessageContent($data)
{
switch ($data['type']) {
case 'text':
return $data['text']['body'];
@@ -280,34 +326,36 @@ class WhatsAppService {
}
return json_encode($data);
}
/**
* Obtener usuario por teléfono
*/
private function getUserByPhone($phone) {
private function getUserByPhone($phone)
{
return $this->db->fetch(
"SELECT * FROM users WHERE phone_number = :phone",
['phone' => $phone]
);
}
/**
* Descargar archivo multimedia
*/
public function downloadMedia($mediaId) {
public function downloadMedia($mediaId)
{
$url = $this->apiUrl . $mediaId;
// Primero obtener información del archivo
$mediaInfo = $this->makeRequest('GET', $url);
if (!$mediaInfo || !isset($mediaInfo['url'])) {
throw new Exception("No se pudo obtener información del archivo");
}
// Descargar el archivo
$fileUrl = $mediaInfo['url'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $fileUrl,
CURLOPT_RETURNTRANSFER => true,
@@ -317,15 +365,15 @@ class WhatsAppService {
CURLOPT_TIMEOUT => 60,
CURLOPT_FOLLOWLOCATION => true
]);
$fileContent = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("Error descargando archivo multimedia");
}
return [
'content' => $fileContent,
'mime_type' => $mediaInfo['mime_type'] ?? 'application/octet-stream',
@@ -333,4 +381,3 @@ class WhatsAppService {
];
}
}
?>
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* Test directo de plantilla - Debug detallado
*/
require_once 'config/config.php';
require_once 'services/WhatsAppService.php';
header('Content-Type: application/json; charset=utf-8');
// Configurar logging de errores para ver los detalles
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
try {
echo json_encode(['status' => 'Iniciando test de plantilla...'], JSON_PRETTY_PRINT) . "\n";
$whatsapp = new WhatsAppService();
// Usar datos de prueba que sabemos que existen
$recipient = "573168950803"; // Número de prueba
$templateName = "hello_world"; // Esta plantilla FUNCIONA
$language = "en_US"; // Idioma que FUNCIONA
echo json_encode([
'step' => 'Datos de envío',
'recipient' => $recipient,
'template' => $templateName,
'language' => $language
], JSON_PRETTY_PRINT) . "\n";
// Enviar plantilla
echo json_encode(['step' => 'Llamando sendTemplateMessage...'], JSON_PRETTY_PRINT) . "\n";
flush();
$result = $whatsapp->sendTemplateMessage($recipient, $templateName, $language);
echo json_encode([
'step' => 'Resultado',
'success' => true,
'result' => $result
], JSON_PRETTY_PRINT) . "\n";
} catch (Exception $e) {
echo json_encode([
'step' => 'Error capturado',
'success' => false,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
], JSON_PRETTY_PRINT) . "\n";
}
echo json_encode(['status' => 'Test completado. Revisa los logs de error para ver detalles de la petición HTTP.'], JSON_PRETTY_PRINT) . "\n";
?>
+52
View File
@@ -0,0 +1,52 @@
<?php
require_once 'config/config.php';
echo "<h2>Test de Configuración WhatsApp</h2>";
// Obtener configuración desde BD
$config = getWhatsAppConfigFromDB();
echo "<h3>Configuración desde Base de Datos:</h3>";
echo "<pre>";
echo "Token: " . (strlen($config['token']) > 0 ? substr($config['token'], 0, 20) . "... (length: " . strlen($config['token']) . ")" : "EMPTY") . "\n";
echo "Phone Number ID: " . ($config['phone_number_id'] ?: "EMPTY") . "\n";
echo "API URL: " . ($config['api_url'] ?: "EMPTY") . "\n";
echo "</pre>";
// Construir URL como lo haría WhatsAppService
$apiUrl = rtrim($config['api_url'], '/') . '/' . $config['phone_number_id'] . '/messages';
echo "<h3>URL que se construirá:</h3>";
echo "<pre>" . $apiUrl . "</pre>";
// Comparar con tu curl que funciona
$expectedUrl = "https://graph.facebook.com/v22.0/938064202726485/messages";
echo "<h3>URL que debería ser:</h3>";
echo "<pre>" . $expectedUrl . "</pre>";
echo "<h3>¿Coinciden las URLs?</h3>";
echo "<pre>" . ($apiUrl === $expectedUrl ? "✅ SÍ" : "❌ NO") . "</pre>";
// Verificar token
$expectedTokenStart = "EAAcGjPRqunIBQVT6b86O0wWWGLGJ1PANmolIRXRBmEcpqa3P85VlUz0Rrv2c5deXky3gOKZCBw7ZCpdicly4c0G8qFMOH1DfOcSzmV49FlPId3AGPlHEXpqlsPFIE8DF18trW2vglQrFiCHXZCEBSUBQfp1jEASD1NrKKKVHiGVZARrwoxP0J931u0KIr2ZAZCug4fFpnyRZBls9LEgJuomXd4ZCnJtBZA4GvP2zaplmZA52mJiG5H4YiKKWby5GnQq9BgAVaq6sLZBnYI26aR0FgZDZD";
echo "<h3>¿Coincide el token?</h3>";
$tokenMatch = substr($config['token'], 0, 50) === substr($expectedTokenStart, 0, 50);
echo "<pre>" . ($tokenMatch ? "✅ SÍ (primeros 50 caracteres)" : "❌ NO") . "</pre>";
if (!$tokenMatch) {
echo "<h4>Debug Token:</h4>";
echo "<pre>";
echo "Configurado: " . substr($config['token'], 0, 50) . "...\n";
echo "Esperado: " . substr($expectedTokenStart, 0, 50) . "...\n";
echo "</pre>";
}
// Test directo de configuración
echo "<h3>Configuraciones Individuales:</h3>";
echo "<pre>";
foreach(['whatsapp_token', 'whatsapp_phone_number_id', 'whatsapp_api_url'] as $key) {
$value = getConfigFromDB($key, 'NO_ENCONTRADO');
echo "$key: " . ($value === 'NO_ENCONTRADO' ? "❌ NO_ENCONTRADO" : "" . substr($value, 0, 30) . "...") . "\n";
}
echo "</pre>";
?>