diff --git a/api/get_whatsapp_profile.php b/api/get_whatsapp_profile.php new file mode 100644 index 0000000..6e99b20 --- /dev/null +++ b/api/get_whatsapp_profile.php @@ -0,0 +1,60 @@ + false, 'error' => 'Token o Phone Number ID no configurado']); + exit; + } + + $url = "{$apiUrl}/{$phoneId}/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"], + CURLOPT_TIMEOUT => 20, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + throw new Exception("Error de conexión: {$error}"); + } + + $data = json_decode($response, true); + + if ($httpCode !== 200) { + $msg = $data['error']['message'] ?? 'Error desconocido'; + throw new Exception("API error ({$httpCode}): {$msg}"); + } + + // La API devuelve { data: [ {...profile} ] } + $profile = isset($data['data'][0]) ? $data['data'][0] : $data; + + echo json_encode(['success' => true, 'data' => $profile]); + +} catch (Exception $e) { + error_log('[get_whatsapp_profile] ' . $e->getMessage()); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/reset_and_sync_templates.php b/api/reset_and_sync_templates.php new file mode 100644 index 0000000..e30f5d4 --- /dev/null +++ b/api/reset_and_sync_templates.php @@ -0,0 +1,177 @@ + false, 'error' => 'Método no permitido']); + exit; +} + +try { + $db = Database::getInstance(); + + // Obtener configuración de WhatsApp + $token = getConfigFromDB('whatsapp_token', ''); + $wabaId = getConfigFromDB('whatsapp_business_account_id', ''); + $config = getWhatsAppConfigFromDB(); + $apiUrl = rtrim($config['api_url'] ?: 'https://graph.facebook.com/v22.0/', '/'); + + if (empty($token)) { + throw new Exception('Token de WhatsApp no configurado'); + } + if (empty($wabaId)) { + throw new Exception('Business Account ID no configurado. Configúrelo en Configuración del Sistema.'); + } + + // ── Paso 1: Obtener plantillas desde Facebook ───────────────────────────── + $url = "{$apiUrl}/{$wabaId}/message_templates"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}", "Content-Type: application/json"], + CURLOPT_TIMEOUT => 30, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErr = curl_error($ch); + curl_close($ch); + + if ($curlErr) { + throw new Exception("Error de conexión: {$curlErr}"); + } + if ($httpCode !== 200) { + $errData = json_decode($response, true); + $errMsg = $errData['error']['message'] ?? 'Error desconocido'; + throw new Exception("Error de API ({$httpCode}): {$errMsg}"); + } + + $data = json_decode($response, true); + if (!isset($data['data']) || !is_array($data['data'])) { + throw new Exception('Respuesta inválida de la API de WhatsApp'); + } + + $templates = $data['data']; + $totalFromFacebook = count($templates); + + // ── Paso 2: Eliminar TODAS las plantillas locales ───────────────────────── + $deletedCount = $db->query("DELETE FROM message_templates"); + error_log("[reset_and_sync_templates] Eliminadas todas las plantillas locales. Iniciando re-import de {$totalFromFacebook} plantillas."); + + // ── Paso 3: Insertar todas las plantillas desde Facebook ────────────────── + $syncedCount = 0; + $errors = []; + + foreach ($templates as $template) { + try { + $templateName = $template['name'] ?? ''; + $language = $template['language'] ?? 'es'; + $status = $template['status'] ?? 'pending'; + $category = $template['category'] ?? 'UTILITY'; + $components = $template['components'] ?? []; + + if (empty($templateName)) continue; + + // Extraer campos de componentes + $bodyText = null; + $headerText = null; + $headerType = null; + $footerText = null; + $exampleParameters = []; + + foreach ($components as $comp) { + $type = $comp['type'] ?? ''; + if ($type === 'BODY') { + $bodyText = $comp['text'] ?? null; + if (isset($comp['example']['body_text'])) { + $exampleParameters['body'] = $comp['example']['body_text']; + } + } elseif ($type === 'HEADER') { + $headerText = $comp['text'] ?? null; + $headerType = strtolower($comp['format'] ?? 'text'); + if (isset($comp['example']['header_text'])) { + $exampleParameters['header'] = $comp['example']['header_text']; + } + } elseif ($type === 'FOOTER') { + $footerText = $comp['text'] ?? null; + } + } + + // Extraer variables automáticamente del body + $variables = []; + if ($bodyText) { + preg_match_all('/\{\{([^\}]+)\}\}/', $bodyText, $matches); + if (!empty($matches[1])) { + $index = 1; + foreach (array_unique($matches[1]) as $varName) { + $varIndex = is_numeric($varName) ? (int)$varName : $index; + $exData = $exampleParameters['body'][$varIndex - 1] ?? null; + $example = is_array($exData) ? $exData[0] : $exData; + $variables[] = [ + 'index' => $varIndex, + 'placeholder' => "{{" . $varName . "}}", + 'name' => $varName, + 'example' => $example, + ]; + if (!is_numeric($varName)) $index++; + } + $exampleParameters['variables'] = $variables; + } + } + + $componentsJson = !empty($components) ? json_encode($components, JSON_UNESCAPED_UNICODE) : null; + $exampleJson = !empty($exampleParameters) ? json_encode($exampleParameters, JSON_UNESCAPED_UNICODE) : null; + + $db->execute( + "INSERT INTO message_templates ( + name, template_name, language_code, category, status, + body_text, header_text, header_type, footer_text, + components, example_parameters, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())", + [ + $templateName, $templateName, + $language, strtolower($category), strtolower($status), + $bodyText, $headerText, $headerType, $footerText, + $componentsJson, $exampleJson, + ] + ); + + $syncedCount++; + } catch (Exception $e) { + $errors[] = "Error con '{$templateName}': " . $e->getMessage(); + error_log("[reset_and_sync_templates] " . end($errors)); + } + } + + writeLog('INFO', "Reset+Sync plantillas: {$syncedCount}/{$totalFromFacebook} importadas, " . count($errors) . " errores"); + + echo json_encode([ + 'success' => true, + 'message' => "Re-sincronización completada: {$syncedCount} plantillas importadas", + 'data' => [ + 'total_facebook' => $totalFromFacebook, + 'deleted_local' => $deletedCount, + 'imported' => $syncedCount, + 'errors' => $errors, + ], + ]); + +} catch (Exception $e) { + error_log('[reset_and_sync_templates] ' . $e->getMessage()); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/update_whatsapp_profile.php b/api/update_whatsapp_profile.php new file mode 100644 index 0000000..0d283d9 --- /dev/null +++ b/api/update_whatsapp_profile.php @@ -0,0 +1,87 @@ + false, 'error' => 'Método no permitido']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true) ?? $_POST; + +try { + $config = getWhatsAppConfigFromDB(); + $token = $config['token'] ?? ''; + $phoneId = $config['phone_number_id'] ?? ''; + $apiUrl = rtrim($config['api_url'] ?: 'https://graph.facebook.com/v22.0/', '/'); + + if (empty($token) || empty($phoneId)) { + echo json_encode(['success' => false, 'error' => 'Token o Phone Number ID no configurado']); + exit; + } + + // Construir payload solo con campos presentes y no vacíos + $payload = ['messaging_product' => 'whatsapp']; + + $allowed = ['about', 'address', 'description', 'email', 'vertical']; + foreach ($allowed as $field) { + if (isset($input[$field])) { + $payload[$field] = trim($input[$field]); + } + } + + // websites es un array + if (!empty($input['websites'])) { + $websites = array_values(array_filter(array_map('trim', (array)$input['websites']))); + if (!empty($websites)) { + $payload['websites'] = $websites; + } + } + + $url = "{$apiUrl}/{$phoneId}/whatsapp_business_profile"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer {$token}", + "Content-Type: application/json", + ], + CURLOPT_TIMEOUT => 20, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + throw new Exception("Error de conexión: {$error}"); + } + + $data = json_decode($response, true); + + if ($httpCode !== 200) { + $msg = $data['error']['message'] ?? 'Error desconocido'; + throw new Exception("API error ({$httpCode}): {$msg}"); + } + + echo json_encode(['success' => true, 'message' => 'Perfil actualizado correctamente']); + +} catch (Exception $e) { + error_log('[update_whatsapp_profile] ' . $e->getMessage()); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/upload_whatsapp_profile_photo.php b/api/upload_whatsapp_profile_photo.php new file mode 100644 index 0000000..6276557 --- /dev/null +++ b/api/upload_whatsapp_profile_photo.php @@ -0,0 +1,134 @@ + false, 'error' => 'Método no permitido']); + exit; +} + +if (empty($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK) { + $code = $_FILES['photo']['error'] ?? -1; + echo json_encode(['success' => false, 'error' => "No se recibió archivo válido (código {$code})"]); + exit; +} + +$file = $_FILES['photo']; + +// Validar tipo MIME +$allowedMimes = ['image/jpeg', 'image/png']; +$finfo = new finfo(FILEINFO_MIME_TYPE); +$mime = $finfo->file($file['tmp_name']); +if (!in_array($mime, $allowedMimes, true)) { + echo json_encode(['success' => false, 'error' => 'Solo se permiten imágenes JPG o PNG']); + exit; +} + +// Validar tamaño (máx 5 MB) +if ($file['size'] > 5 * 1024 * 1024) { + echo json_encode(['success' => false, 'error' => 'La imagen no puede superar 5 MB']); + exit; +} + +try { + $config = getWhatsAppConfigFromDB(); + $token = $config['token'] ?? ''; + $phoneId = $config['phone_number_id'] ?? ''; + $apiUrl = rtrim($config['api_url'] ?: 'https://graph.facebook.com/v22.0/', '/'); + + if (empty($token) || empty($phoneId)) { + echo json_encode(['success' => false, 'error' => 'Token o Phone Number ID no configurado']); + exit; + } + + // ── Paso 1: subir la imagen como media ─────────────────────────────────── + $uploadUrl = "{$apiUrl}/{$phoneId}/media"; + + $cfile = new CURLFile($file['tmp_name'], $mime, basename($file['name'])); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $uploadUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => [ + 'messaging_product' => 'whatsapp', + 'file' => $cfile, + 'type' => $mime, + ], + CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"], + CURLOPT_TIMEOUT => 30, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ]); + + $uploadResponse = curl_exec($ch); + $uploadCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $uploadError = curl_error($ch); + curl_close($ch); + + if ($uploadError) { + throw new Exception("Error al subir imagen: {$uploadError}"); + } + + $uploadData = json_decode($uploadResponse, true); + if ($uploadCode !== 200 || empty($uploadData['id'])) { + $msg = $uploadData['error']['message'] ?? 'Error desconocido al subir'; + throw new Exception("Upload error ({$uploadCode}): {$msg}"); + } + + $mediaHandle = $uploadData['id']; + + // ── Paso 2: actualizar la foto de perfil con el handle ─────────────────── + $profileUrl = "{$apiUrl}/{$phoneId}/whatsapp_business_profile"; + + $payload = [ + 'messaging_product' => 'whatsapp', + 'profile_picture_handle' => $mediaHandle, + ]; + + $ch2 = curl_init(); + curl_setopt_array($ch2, [ + CURLOPT_URL => $profileUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer {$token}", + "Content-Type: application/json", + ], + CURLOPT_TIMEOUT => 20, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ]); + + $profileResponse = curl_exec($ch2); + $profileCode = curl_getinfo($ch2, CURLINFO_HTTP_CODE); + $profileError = curl_error($ch2); + curl_close($ch2); + + if ($profileError) { + throw new Exception("Error al actualizar foto: {$profileError}"); + } + + $profileData = json_decode($profileResponse, true); + if ($profileCode !== 200) { + $msg = $profileData['error']['message'] ?? 'Error desconocido al actualizar foto'; + throw new Exception("Profile photo error ({$profileCode}): {$msg}"); + } + + echo json_encode(['success' => true, 'message' => 'Foto de perfil actualizada correctamente']); + +} catch (Exception $e) { + error_log('[upload_whatsapp_profile_photo] ' . $e->getMessage()); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/js/app_simple.js b/assets/js/app_simple.js index 2ce2e54..74e0ee9 100644 --- a/assets/js/app_simple.js +++ b/assets/js/app_simple.js @@ -117,6 +117,11 @@ class SimpleWhatsAppManager { case 'templates': this.loadTemplates(); break; + case 'whatsapp_profile': + if (typeof window.loadWhatsAppProfile === 'function') { + window.loadWhatsAppProfile(); + } + break; case 'broadcast': // Recargar plantillas cuando se abre el tab de broadcast if (typeof loadBroadcastTemplates === 'function') { @@ -5098,4 +5103,160 @@ window.toggleAdminUserStatus = async function(userId, isActive) { console.error(`Error ${action}ando usuario:`, error); alert(`Error al ${action} usuario: ` + error.message); } + +// ───────────────────────────────────────────────────────────────────────────── +// PERFIL DE WHATSAPP BUSINESS +// ───────────────────────────────────────────────────────────────────────────── + +window.loadWhatsAppProfile = async function() { + if (!window.whatsappManager) return; + window.whatsappManager.showInfo('Cargando perfil desde WhatsApp...'); + try { + const res = await window.whatsappManager.apiCall('get_whatsapp_profile.php'); + if (!res || !res.success) { + window.whatsappManager.showError(res?.error || 'Error al obtener el perfil'); + return; + } + const p = res.data || {}; + + document.getElementById('wp-about').value = p.about || ''; + document.getElementById('wp-description').value = p.description || ''; + document.getElementById('wp-address').value = p.address || ''; + document.getElementById('wp-email').value = p.email || ''; + document.getElementById('wp-website').value = (p.websites && p.websites[0]) ? p.websites[0] : ''; + + const vertSel = document.getElementById('wp-vertical'); + if (vertSel && p.vertical) { + vertSel.value = p.vertical; + } + + // Foto de perfil + const img = document.getElementById('wp-profile-photo-preview'); + const placeholder = document.getElementById('wp-profile-photo-placeholder'); + if (p.profile_picture_url && img) { + img.src = p.profile_picture_url; + img.style.display = 'inline-block'; + if (placeholder) placeholder.style.display = 'none'; + } + + window.whatsappManager.showSuccess('Perfil cargado correctamente'); + } catch (e) { + window.whatsappManager.showError('Error: ' + e.message); + } +}; + +window.saveWhatsAppProfile = async function() { + if (!window.whatsappManager) return; + + const payload = { + about: document.getElementById('wp-about')?.value?.trim() || '', + description: document.getElementById('wp-description')?.value?.trim() || '', + address: document.getElementById('wp-address')?.value?.trim() || '', + email: document.getElementById('wp-email')?.value?.trim() || '', + vertical: document.getElementById('wp-vertical')?.value || '', + websites: [document.getElementById('wp-website')?.value?.trim()].filter(Boolean), + }; + + try { + window.whatsappManager.showInfo('Guardando perfil...'); + const res = await window.whatsappManager.apiCall('update_whatsapp_profile.php', { + method: 'POST', + body: payload, + }); + if (res && res.success) { + window.whatsappManager.showSuccess(res.message || 'Perfil actualizado correctamente'); + } else { + window.whatsappManager.showError(res?.error || 'Error al guardar el perfil'); + } + } catch (e) { + window.whatsappManager.showError('Error: ' + e.message); + } +}; + +window.uploadWhatsAppProfilePhoto = async function() { + const fileInput = document.getElementById('wp-photo-file'); + if (!fileInput || !fileInput.files || fileInput.files.length === 0) { + alert('Selecciona una foto primero'); + return; + } + + const formData = new FormData(); + formData.append('photo', fileInput.files[0]); + + try { + window.whatsappManager.showInfo('Subiendo foto de perfil...'); + const baseUrl = window.whatsappManager.baseUrl || 'api/'; + const response = await fetch(baseUrl + 'upload_whatsapp_profile_photo.php', { + method: 'POST', + body: formData, + credentials: 'same-origin', + }); + + const res = await response.json(); + if (res && res.success) { + window.whatsappManager.showSuccess(res.message || 'Foto actualizada correctamente'); + // Actualizar preview local + const reader = new FileReader(); + reader.onload = (e) => { + const img = document.getElementById('wp-profile-photo-preview'); + const ph = document.getElementById('wp-profile-photo-placeholder'); + if (img) { img.src = e.target.result; img.style.display = 'inline-block'; } + if (ph) { ph.style.display = 'none'; } + }; + reader.readAsDataURL(fileInput.files[0]); + fileInput.value = ''; + } else { + window.whatsappManager.showError(res?.error || 'Error al subir la foto'); + } + } catch (e) { + window.whatsappManager.showError('Error: ' + e.message); + } +}; + +// Cargar perfil al abrir la pestaña +document.addEventListener('DOMContentLoaded', () => { + const profileLinks = document.querySelectorAll('[data-tab="whatsapp_profile"]'); + profileLinks.forEach(link => { + link.addEventListener('click', () => { + // Pequeño delay para que el tab esté visible + setTimeout(() => window.loadWhatsAppProfile && window.loadWhatsAppProfile(), 100); + }); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// RESET + RE-SINCRONIZACIÓN DE PLANTILLAS +// ───────────────────────────────────────────────────────────────────────────── + +window.resetAndSyncTemplates = async function() { + if (!window.whatsappManager) { alert('Error: Sistema no inicializado'); return; } + + const confirmed = confirm( + '⚠️ ATENCIÓN: Esta acción eliminará TODAS las plantillas locales y las volverá a importar desde WhatsApp/Facebook.\n\n' + + '¿Estás seguro de que deseas continuar?' + ); + if (!confirmed) return; + + const btn = document.getElementById('btn-reset-sync-templates'); + if (btn) { btn.disabled = true; btn.innerHTML = ' Procesando...'; } + + try { + window.whatsappManager.showInfo('Eliminando plantillas locales y re-sincronizando desde WhatsApp...'); + + const response = await window.whatsappManager.apiCall('reset_and_sync_templates.php', { method: 'POST' }); + + if (response && response.success) { + const d = response.data || {}; + const msg = `✅ ${response.message}\n• Recibidas de Facebook: ${d.total_facebook || 0}\n• Importadas: ${d.imported || 0}${d.errors?.length ? '\n⚠️ ' + d.errors.length + ' errores' : ''}`; + window.whatsappManager.showSuccess(msg); + if (window.whatsappManager.loadTemplates) window.whatsappManager.loadTemplates(); + } else { + window.whatsappManager.showError(response?.error || 'Error desconocido al re-sincronizar'); + } + } catch (e) { + window.whatsappManager.showError('Error de conexión: ' + e.message); + } finally { + if (btn) { btn.disabled = false; btn.innerHTML = ' Limpiar y Re-sincronizar'; } + } +}; }; \ No newline at end of file diff --git a/index.php b/index.php index c44f67d..8f15806 100644 --- a/index.php +++ b/index.php @@ -58,6 +58,7 @@ try {
  • Mensajes Programados
  • Plantillas
  • Respuestas Auto
  • +
  • Perfil WhatsApp
  • Configuración
  • Logs
  • T&C Aceptaciones
  • @@ -664,6 +665,98 @@ try { + +
    +
    +
    +
    Perfil de WhatsApp Business
    + +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + + + JPG o PNG, máx. 5 MB +
    + +
    + + +
    +
    +
    + + + Máx. 139 caracteres +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    @@ -673,6 +766,9 @@ try { +