From b1961f0116cd006ea835d93c323fda14c0144c13 Mon Sep 17 00:00:00 2001 From: lizandrogd <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 21 Jan 2026 02:22:29 -0500 Subject: [PATCH] up --- api/debug_webhook_init.php | 16 ++ api/delete_conversation.php | 30 +++ api/delete_template.php | 25 +++ api/get_conversations.php | 32 +-- api/get_templates.php | 29 +-- api/get_user_messages.php | 9 +- api/mark_conversation_read.php | 22 ++ api/send_message.php | 39 +--- api/update_user.php | 49 +++-- api/webhook.php | 28 ++- assets/js/app.js | 10 +- conversations.php | 260 ++++++++++++++++++++--- logs/system.log | 12 ++ scripts/add_conversation_fields.php | 36 ++++ scripts/add_message_is_read.php | 15 ++ scripts/add_reaction_to_message_type.php | 26 +++ scripts/add_user_avatar_column.php | 15 ++ scripts/db_debug.php | 10 + scripts/db_test.php | 13 ++ scripts/debug_init_service.php | 24 +++ scripts/describe_conversations.php | 7 + scripts/describe_users.php | 7 + scripts/run_webhook_get.php | 24 +++ scripts/run_webhook_out.txt | 0 scripts/test_reaction_and_reply.php | 121 +++++++++++ scripts/test_require.php | 12 ++ scripts/wa_out.txt | Bin 0 -> 1670 bytes scripts/wa_service_test.php | 16 ++ scripts/webhook_test_cli.php | 13 ++ services/BotService.php | 10 +- services/WhatsAppService.php | 11 + tests/webhook_tests.php | 121 ++++++++++- tests/webhook_tests_out.txt | Bin 0 -> 264 bytes tests/whatsapp_service_tests.php | 86 ++++++++ tests/whatsapp_service_tests_out.txt | Bin 0 -> 318 bytes 35 files changed, 1006 insertions(+), 122 deletions(-) create mode 100644 api/debug_webhook_init.php create mode 100644 api/delete_conversation.php create mode 100644 api/delete_template.php create mode 100644 api/mark_conversation_read.php create mode 100644 scripts/add_conversation_fields.php create mode 100644 scripts/add_message_is_read.php create mode 100644 scripts/add_reaction_to_message_type.php create mode 100644 scripts/add_user_avatar_column.php create mode 100644 scripts/db_debug.php create mode 100644 scripts/db_test.php create mode 100644 scripts/debug_init_service.php create mode 100644 scripts/describe_conversations.php create mode 100644 scripts/describe_users.php create mode 100644 scripts/run_webhook_get.php create mode 100644 scripts/run_webhook_out.txt create mode 100644 scripts/test_reaction_and_reply.php create mode 100644 scripts/test_require.php create mode 100644 scripts/wa_out.txt create mode 100644 scripts/wa_service_test.php create mode 100644 scripts/webhook_test_cli.php create mode 100644 tests/webhook_tests_out.txt create mode 100644 tests/whatsapp_service_tests.php create mode 100644 tests/whatsapp_service_tests_out.txt diff --git a/api/debug_webhook_init.php b/api/debug_webhook_init.php new file mode 100644 index 0000000..acc2d72 --- /dev/null +++ b/api/debug_webhook_init.php @@ -0,0 +1,16 @@ + true, 'message' => 'Webhook class instantiated successfully']); +} catch (Throwable $t) { + http_response_code(500); + error_log('[debug_webhook_init] Throwable: ' . $t->getMessage()); + error_log($t->getTraceAsString()); + echo json_encode(['ok' => false, 'error' => $t->getMessage(), 'trace' => $t->getTraceAsString()]); +} diff --git a/api/delete_conversation.php b/api/delete_conversation.php new file mode 100644 index 0000000..9b06884 --- /dev/null +++ b/api/delete_conversation.php @@ -0,0 +1,30 @@ + false, 'error' => 'conversation_id or user_id required']); + exit; +} + +try { + $db = Database::getInstance(); + if ($conversationId) { + $db->execute('DELETE FROM conversations WHERE id = ?', [$conversationId]); + } + if ($userId) { + $db->execute('DELETE FROM conversations WHERE user_id = ?', [$userId]); + } + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/delete_template.php b/api/delete_template.php new file mode 100644 index 0000000..3e6b421 --- /dev/null +++ b/api/delete_template.php @@ -0,0 +1,25 @@ + false, 'error' => 'id is required']); + exit; +} + +try { + $db = Database::getInstance(); + $db->execute("DELETE FROM message_templates WHERE id = ?", [$id]); + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/get_conversations.php b/api/get_conversations.php index 6350c1b..48688df 100644 --- a/api/get_conversations.php +++ b/api/get_conversations.php @@ -14,21 +14,29 @@ header('Access-Control-Allow-Headers: Content-Type'); try { $db = Database::getInstance(); - // Obtener conversaciones de la tabla conversations + // Obtener conversaciones agregadas por usuario: último mensaje + conteo de no leídos + avatar $conversations = $db->fetchAll( "SELECT - c.id, - c.user_id, - COALESCE(c.content, '') as content, - c.direction, - c.message_type, - c.status, - c.created_at, + u.id AS user_id, + COALESCE(u.name, u.phone_number) AS name, u.phone_number, - COALESCE(u.name, u.phone_number) as name - FROM conversations c - JOIN users u ON c.user_id = u.id - ORDER BY c.created_at DESC + u.avatar_url, + lm.message_id AS last_message_id, + lm.content AS last_message, + lm.direction AS direction, + lm.message_type AS message_type, + lm.created_at AS last_time, + IFNULL(SUM(CASE WHEN c.direction = 'incoming' AND (c.is_read IS NULL OR c.is_read = 0) THEN 1 ELSE 0 END), 0) AS unread_count + FROM users u + LEFT JOIN conversations c ON c.user_id = u.id + LEFT JOIN ( + SELECT t1.* FROM conversations t1 + JOIN ( + SELECT user_id, MAX(created_at) AS last_time FROM conversations GROUP BY user_id + ) t2 ON t1.user_id = t2.user_id AND t1.created_at = t2.last_time + ) lm ON lm.user_id = u.id + GROUP BY u.id + ORDER BY lm.created_at DESC LIMIT 500" ); diff --git a/api/get_templates.php b/api/get_templates.php index a8b88c9..62e0e11 100644 --- a/api/get_templates.php +++ b/api/get_templates.php @@ -24,18 +24,23 @@ header('Access-Control-Allow-Headers: Content-Type'); try { $db = Database::getInstance(); - $templates = $db->fetchAll( - "SELECT - id, - name, - template_name, - language_code, - category, - status, - created_at - FROM message_templates - ORDER BY name ASC" - ); + $approvedOnly = isset($_GET['approved_only']) && $_GET['approved_only'] == '1'; + $limit = isset($_GET['limit']) ? intval($_GET['limit']) : null; + + $sql = "SELECT id, name, template_name, language_code, category, status, body_text, created_at FROM message_templates"; + $params = []; + + if ($approvedOnly) { + $sql .= " WHERE status = 'approved'"; + } + + $sql .= " ORDER BY name ASC"; + + if ($limit && $limit > 0) { + $sql .= " LIMIT " . $limit; + } + + $templates = $db->fetchAll($sql, $params); echo json_encode([ 'success' => true, diff --git a/api/get_user_messages.php b/api/get_user_messages.php index 83f41b2..6ed0327 100644 --- a/api/get_user_messages.php +++ b/api/get_user_messages.php @@ -37,7 +37,8 @@ try { direction, message_type, status, - created_at + created_at, + COALESCE(c.is_read, 0) as is_read FROM conversations c LEFT JOIN users u ON c.user_id = u.id WHERE user_id = :user_id @@ -46,10 +47,14 @@ try { id, user_id, message_text as content, + NULL as message_id, + NULL as media_url, + NULL as user_phone, direction, message_type, status, - created_at + created_at, + 1 as is_read FROM messages WHERE user_id = :user_id ORDER BY created_at ASC", diff --git a/api/mark_conversation_read.php b/api/mark_conversation_read.php new file mode 100644 index 0000000..c46145f --- /dev/null +++ b/api/mark_conversation_read.php @@ -0,0 +1,22 @@ + false, 'error' => 'user_id is required']); + exit; + } + + $db = Database::getInstance(); + $db->execute("UPDATE conversations SET is_read = 1 WHERE user_id = ? AND direction = 'incoming' AND (is_read IS NULL OR is_read = 0)", [$userId]); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/send_message.php b/api/send_message.php index c66b1c7..4f446cd 100644 --- a/api/send_message.php +++ b/api/send_message.php @@ -371,13 +371,8 @@ try { $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] - ); - - error_log("Mensaje guardado en BD - User ID: $userId, Content: $messageContent"); + // Guardado en BD manejado por WhatsAppService->saveOutgoingMessage() + error_log("Outgoing message saving delegated to WhatsAppService - User ID: $userId, Content: $messageContent"); } else { error_log("Error: No se pudo obtener o crear user_id para recipient: $recipient"); } @@ -476,35 +471,7 @@ try { $response = $whatsappService->sendTextMessage($user['phone_number'], $message); if ($response && isset($response['messages']) && !empty($response['messages'])) { - // Guardar mensaje en la base de datos - $messageData = [ - 'user_id' => $userId, - 'content' => $message, - 'direction' => 'outgoing', - 'message_type' => 'text', - 'status' => 'sent', - 'message_id' => $response['messages'][0]['id'] ?? null, - 'created_at' => date('Y-m-d H:i:s') - ]; - - // Intentar insertar en conversations primero - try { - $stmt = $db->query( - "INSERT INTO conversations (user_id, content, direction, message_type, status, message_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", - array_values($messageData) - ); - } catch (Exception $e) { - // Si falla, intentar en messages - try { - $stmt = $db->query( - "INSERT INTO messages (user_id, message_text, direction, message_type, status, message_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", - [$userId, $message, 'outgoing', 'text', 'sent', $response['messages'][0]['id'] ?? null, date('Y-m-d H:i:s')] - ); - } catch (Exception $e2) { - error_log("Error guardando mensaje: " . $e2->getMessage()); - } - } - + // Saving handled by WhatsAppService->saveOutgoingMessage(), skip manual insert to avoid duplicates echo json_encode([ 'success' => true, 'message' => 'Mensaje enviado correctamente', diff --git a/api/update_user.php b/api/update_user.php index f01ebb7..0e0effa 100644 --- a/api/update_user.php +++ b/api/update_user.php @@ -42,12 +42,13 @@ try { throw new Exception('Datos inválidos'); } - // Validar datos requeridos - if (!isset($data['user_id']) || empty($data['user_id'])) { - throw new Exception('ID de usuario requerido'); + // Validar datos requeridos: user_id o phone + if ((!isset($data['user_id']) || empty($data['user_id'])) && (!isset($data['phone']) || empty($data['phone']))) { + throw new Exception('ID de usuario o teléfono requerido'); } - $user_id = intval($data['user_id']); + $user_id = isset($data['user_id']) ? intval($data['user_id']) : null; + $phone = isset($data['phone']) ? trim($data['phone']) : null; $name = isset($data['name']) && !empty(trim($data['name'])) ? trim($data['name']) : null; $status = isset($data['status']) ? $data['status'] : 'active'; @@ -60,20 +61,34 @@ try { // Crear conexión a la base de datos usando el patrón Singleton $db = Database::getInstance(); - // Verificar si el usuario existe - $checkUser = $db->fetch("SELECT id FROM users WHERE id = :user_id", ['user_id' => $user_id]); - - if (!$checkUser) { - throw new Exception('Usuario no encontrado'); - } + // Si tenemos user_id - actualizar + if ($user_id) { + // Verificar si el usuario existe + $checkUser = $db->fetch("SELECT id FROM users WHERE id = :user_id", ['user_id' => $user_id]); + + if (!$checkUser) { + throw new Exception('Usuario no encontrado'); + } - // Actualizar usuario usando el método de la clase Database - $updateData = [ - 'name' => $name, - 'status' => $status - ]; - - $result = $db->update('users', $updateData, 'id = :user_id', ['user_id' => $user_id]); + // Actualizar usuario usando el método de la clase Database + $updateData = [ + 'name' => $name, + 'status' => $status + ]; + + $result = $db->update('users', $updateData, 'id = :user_id', ['user_id' => $user_id]); + + } else { + // Buscar por teléfono y crear/actualizar + $existing = $db->fetch('SELECT id FROM users WHERE phone_number = :p LIMIT 1', ['p' => $phone]); + if ($existing) { + $result = $db->update('users', ['name' => $name, 'status' => $status], 'id = :id', ['id' => $existing['id']]); + $user_id = $existing['id']; + } else { + $user_id = $db->insert('users', ['phone_number' => $phone, 'name' => $name, 'status' => $status, 'created_at' => date('Y-m-d H:i:s')]); + $result = $user_id ? true : false; + } + } if ($result === false) { throw new Exception('No se pudo actualizar el usuario'); diff --git a/api/webhook.php b/api/webhook.php index 2108c8c..494a962 100644 --- a/api/webhook.php +++ b/api/webhook.php @@ -100,7 +100,7 @@ class WhatsAppWebhook { if (!isset($value['messages'])) { return; } - + foreach ($value['messages'] as $message) { $phoneNumber = $message['from']; $messageId = $message['id']; @@ -134,6 +134,13 @@ class WhatsAppWebhook { $emoji = $message['reaction']['emoji'] ?? ''; $reactionTo = $message['reaction']['message_id'] ?? ''; $messageText = json_encode(['emoji' => $emoji, 'message_id' => $reactionTo]); + + // Guardar campos específicos para reacciones + $extraFields = [ + 'reaction_to_message_id' => $reactionTo ? intval($reactionTo) : null, + 'reaction_emoji' => $emoji + ]; + } elseif (isset($message['text'])) { $messageText = $message['text']['body']; $messageType = 'text'; @@ -155,15 +162,28 @@ class WhatsAppWebhook { } // Guardar mensaje en base de datos - $this->saveMessage([ + $saveData = [ 'user_id' => $user['id'], 'message_id' => $messageId, 'direction' => 'incoming', 'message_type' => $messageType, 'content' => $messageText, 'media_url' => $mediaUrl, - 'status' => 'received' - ]); + 'status' => 'received', + 'is_read' => 0 + ]; + + // Si el mensaje incluye contexto (es respuesta a otro mensaje) + if (isset($message['context']) && isset($message['context']['id'])) { + $saveData['reply_to_message_id'] = $message['context']['id']; + } + + // Añadir campos extra (si se detectaron) + if (isset($extraFields) && is_array($extraFields)) { + $saveData = array_merge($saveData, $extraFields); + } + + $this->saveMessage($saveData); // Procesar con bot (protección contra excepciones externas) try { diff --git a/assets/js/app.js b/assets/js/app.js index 40265e9..208f17c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1245,9 +1245,13 @@ class WhatsAppBotManager { try { this.showLoading('Eliminando plantilla...'); - // Por ahora usaremos update_template_status para marcar como eliminada - // En el futuro se puede crear delete_template.php - this.showError('Función de eliminar aún no implementada'); + const result = await this.apiCall('delete_template.php', 'POST', { id: templateId }); + if (result && result.success) { + this.showSuccess('Plantilla eliminada'); + this.loadTemplates(); + } else { + this.showError(result.error || 'Error eliminando plantilla'); + } } catch (error) { this.showError('Error eliminando plantilla: ' + error.message); } finally { diff --git a/conversations.php b/conversations.php index 2c6f25c..126b0de 100644 --- a/conversations.php +++ b/conversations.php @@ -398,6 +398,31 @@ transform: translateX(-100%); } } + + .reply-preview { + background: #f4f6f8; + border-left: 3px solid #d0d7de; + padding: 6px 8px; + margin-bottom: 6px; + font-size: 12px; + color: #555; + border-radius: 4px; + } + + .reaction-badge { + display: inline-block; + background: rgba(0,0,0,0.06); + padding: 2px 6px; + border-radius: 12px; + font-size: 12px; + margin-top: 8px; + } + + .conversation-avatar img, .conversation-avatar-img { max-width: 44px; max-height:44px; border-radius:50%; } + .conversation-avatar-wrapper { width:48px; display:flex; align-items:center; justify-content:center } + .conversation-item.unread { background: rgba(255, 248, 220, 0.9); } + .unread-badge { margin-left: 8px; font-size: 12px; } +
@@ -446,9 +471,12 @@