configuracion: sección perfil WhatsApp Business (foto, about, dirección, email, webs, categoría)
- wa_profile.php: GET carga perfil desde Graph API, POST guardar campos, POST foto sube imagen - configuracion.php tab sesión: UI con foto de perfil editable, campos del negocio y botón guardar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
eddfe4d16c
commit
707ef24572
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* GET → Lee el perfil de negocio del número WA turnero desde la Graph API
|
||||
* POST accion=guardar → Actualiza campos del perfil (about, description, address, email, websites, vertical)
|
||||
* POST accion=foto → Sube imagen y la aplica como foto de perfil
|
||||
*/
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
requireTurnero();
|
||||
require_once __DIR__ . '/../../../services/WhatsAppService.php';
|
||||
|
||||
$token = getConfigFromDB('whatsapp_token', '');
|
||||
$phoneId = getConfigFromDB('whatsapp_phone_number_id_turnero', '')
|
||||
?: getConfigFromDB('whatsapp_phone_number_id', '');
|
||||
$apiBase = 'https://graph.facebook.com/v22.0/';
|
||||
|
||||
if (!$token || !$phoneId) jsonError('Token o Phone ID del turnero no configurados.', 422);
|
||||
|
||||
// ── GET: cargar perfil actual ────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$fields = 'about,address,description,email,profile_picture_url,websites,vertical,messaging_product';
|
||||
$url = $apiBase . $phoneId . '/whatsapp_business_profile?fields=' . $fields
|
||||
. '&access_token=' . urlencode($token);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
if ($http !== 200 || isset($data['error'])) {
|
||||
jsonError($data['error']['message'] ?? 'Error al leer perfil de WhatsApp', $http);
|
||||
}
|
||||
|
||||
$perfil = $data['data'][0] ?? $data;
|
||||
jsonOk(['perfil' => $perfil]);
|
||||
}
|
||||
|
||||
// ── POST ────────────────────────────────────────────────────
|
||||
requireMethod('POST');
|
||||
|
||||
// Detectar si viene como multipart (foto) o JSON
|
||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
$esMultipart = str_contains($contentType, 'multipart/form-data');
|
||||
|
||||
if ($esMultipart) {
|
||||
$accion = $_POST['accion'] ?? '';
|
||||
} else {
|
||||
$datos = inputJson();
|
||||
$accion = $datos['accion'] ?? '';
|
||||
}
|
||||
|
||||
// ── POST accion=foto ─────────────────────────────────────────
|
||||
if ($accion === 'foto') {
|
||||
if (empty($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) {
|
||||
jsonError('No se recibió imagen válida.');
|
||||
}
|
||||
$file = $_FILES['foto'];
|
||||
$mime = mime_content_type($file['tmp_name']);
|
||||
$allowed = ['image/jpeg', 'image/png'];
|
||||
if (!in_array($mime, $allowed, true)) jsonError('Solo se aceptan JPG o PNG.');
|
||||
if ($file['size'] > 5 * 1024 * 1024) jsonError('Imagen máxima: 5 MB.');
|
||||
|
||||
// 1. Subir media a WhatsApp
|
||||
$uploadUrl = $apiBase . $phoneId . '/media';
|
||||
$ch = curl_init($uploadUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
|
||||
CURLOPT_POSTFIELDS => [
|
||||
'messaging_product' => 'whatsapp',
|
||||
'type' => $mime,
|
||||
'file' => new CURLFile($file['tmp_name'], $mime, $file['name']),
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$uploadData = json_decode($resp, true);
|
||||
if ($http !== 200 || empty($uploadData['id'])) {
|
||||
jsonError('Error al subir imagen: ' . ($uploadData['error']['message'] ?? $resp));
|
||||
}
|
||||
$mediaHandle = $uploadData['id'];
|
||||
|
||||
// 2. Aplicar como foto de perfil
|
||||
$profileUrl = $apiBase . $phoneId . '/whatsapp_business_profile';
|
||||
$payload = json_encode([
|
||||
'messaging_product' => 'whatsapp',
|
||||
'profile_picture_handle' => $mediaHandle,
|
||||
]);
|
||||
$ch = curl_init($profileUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp2 = curl_exec($ch);
|
||||
$http2 = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$r2 = json_decode($resp2, true);
|
||||
if (isset($r2['error'])) jsonError('Error al aplicar foto: ' . $r2['error']['message']);
|
||||
jsonOk([], 'Foto de perfil actualizada');
|
||||
}
|
||||
|
||||
// ── POST accion=guardar ──────────────────────────────────────
|
||||
if ($accion === 'guardar') {
|
||||
$campos = ['about', 'description', 'address', 'email', 'vertical'];
|
||||
$body = ['messaging_product' => 'whatsapp'];
|
||||
|
||||
foreach ($campos as $c) {
|
||||
$val = trim($datos[$c] ?? '');
|
||||
if ($val !== '') $body[$c] = $val;
|
||||
}
|
||||
// Websites: array de hasta 2
|
||||
$web = array_filter(array_map('trim', (array)($datos['websites'] ?? [])));
|
||||
if ($web) $body['websites'] = array_values($web);
|
||||
|
||||
$url = $apiBase . $phoneId . '/whatsapp_business_profile';
|
||||
$payload = json_encode($body);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$r = json_decode($resp, true);
|
||||
if (isset($r['error'])) jsonError('Error de WhatsApp: ' . $r['error']['message']);
|
||||
jsonOk([], 'Perfil de WhatsApp actualizado');
|
||||
}
|
||||
|
||||
jsonError('Acción no reconocida.');
|
||||
@@ -794,6 +794,87 @@ $tab = $_GET['tab'] ?? 'lugares';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Perfil de WhatsApp Business -->
|
||||
<div class="mt-4">
|
||||
<p class="section-title"><i class="fab fa-whatsapp me-1" style="color:#25d366"></i>Perfil de WhatsApp Business</p>
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<!-- Foto de perfil -->
|
||||
<div style="position:relative;flex-shrink:0">
|
||||
<img id="wa-perfil-foto" src="" alt="Foto perfil"
|
||||
style="width:80px;height:80px;border-radius:50%;object-fit:cover;
|
||||
border:2px solid #dee2e6;background:#f1f5f9;display:none">
|
||||
<div id="wa-perfil-foto-placeholder"
|
||||
style="width:80px;height:80px;border-radius:50%;background:#e2e8f0;
|
||||
display:flex;align-items:center;justify-content:center;font-size:2rem">
|
||||
<i class="fab fa-whatsapp" style="color:#25d366"></i>
|
||||
</div>
|
||||
<label title="Cambiar foto" style="position:absolute;bottom:0;right:0;
|
||||
background:#25d366;color:#fff;border-radius:50%;width:26px;height:26px;
|
||||
display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.8rem">
|
||||
<i class="fas fa-camera"></i>
|
||||
<input type="file" id="wa-foto-input" accept="image/jpeg,image/png"
|
||||
style="display:none" onchange="subirFotoPerfil(this)">
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold" id="wa-perfil-nombre" style="font-size:.95rem">—</div>
|
||||
<div class="text-muted small" id="wa-perfil-about">—</div>
|
||||
<button class="btn btn-outline-secondary btn-sm mt-1" onclick="cargarPerfilWA()">
|
||||
<i class="fas fa-sync-alt me-1"></i>Cargar perfil actual
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-semibold">Descripción corta (About) <small class="text-muted fw-normal">máx. 139 chars</small></label>
|
||||
<input type="text" id="wa-about" maxlength="139" class="form-control form-control-sm"
|
||||
placeholder="Ej: Laboratorio clínico · Lunes a Sábado 7am-5pm">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-semibold">Descripción del negocio</label>
|
||||
<textarea id="wa-description" rows="2" class="form-control form-control-sm"
|
||||
placeholder="Descripción completa del laboratorio…"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Dirección</label>
|
||||
<input type="text" id="wa-address" class="form-control form-control-sm"
|
||||
placeholder="Calle 5 #12-34, Cúcuta">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Email</label>
|
||||
<input type="email" id="wa-email" class="form-control form-control-sm"
|
||||
placeholder="contacto@laboratorio.com">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Sitio web 1</label>
|
||||
<input type="url" id="wa-web1" class="form-control form-control-sm"
|
||||
placeholder="https://www.laboratorio.com">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Sitio web 2 <small class="text-muted fw-normal">(opcional)</small></label>
|
||||
<input type="url" id="wa-web2" class="form-control form-control-sm"
|
||||
placeholder="https://instagram.com/laboratorio">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-semibold">Categoría</label>
|
||||
<select id="wa-vertical" class="form-select form-select-sm">
|
||||
<option value="">— sin especificar —</option>
|
||||
<option value="HEALTH">Salud</option>
|
||||
<option value="MEDICAL_AND_HEALTH">Médico y Salud</option>
|
||||
<option value="BEAUTY">Belleza</option>
|
||||
<option value="EDUCATION">Educación</option>
|
||||
<option value="OTHER">Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end mt-2">
|
||||
<button class="btn btn-success btn-sm" onclick="guardarPerfilWA()">
|
||||
<i class="fas fa-save me-1"></i>Guardar perfil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════════
|
||||
TAB 5 — PANTALLA TV
|
||||
════════════════════════════════════════ -->
|
||||
@@ -1179,6 +1260,80 @@ async function borrarTvVideo() {
|
||||
} catch(e) { toast('Error de conexión', 'error'); }
|
||||
}
|
||||
|
||||
async function cargarPerfilWA() {
|
||||
try {
|
||||
const res = await fetch(API + 'wa_profile.php');
|
||||
const json = await res.json();
|
||||
if (!json.ok) { toast(json.error || 'Error al cargar perfil', 'error'); return; }
|
||||
const p = json.perfil || {};
|
||||
if (p.profile_picture_url) {
|
||||
document.getElementById('wa-perfil-foto').src = p.profile_picture_url;
|
||||
document.getElementById('wa-perfil-foto').style.display = '';
|
||||
document.getElementById('wa-perfil-foto-placeholder').style.display = 'none';
|
||||
}
|
||||
document.getElementById('wa-perfil-about').textContent = p.about || '—';
|
||||
document.getElementById('wa-about').value = p.about || '';
|
||||
document.getElementById('wa-description').value = p.description || '';
|
||||
document.getElementById('wa-address').value = p.address || '';
|
||||
document.getElementById('wa-email').value = p.email || '';
|
||||
const webs = p.websites || [];
|
||||
document.getElementById('wa-web1').value = webs[0] || '';
|
||||
document.getElementById('wa-web2').value = webs[1] || '';
|
||||
if (p.vertical) document.getElementById('wa-vertical').value = p.vertical;
|
||||
toast('Perfil cargado');
|
||||
} catch (e) { toast('Error de conexión', 'error'); }
|
||||
}
|
||||
|
||||
async function guardarPerfilWA() {
|
||||
const body = {
|
||||
accion: 'guardar',
|
||||
about: document.getElementById('wa-about').value.trim(),
|
||||
description: document.getElementById('wa-description').value.trim(),
|
||||
address: document.getElementById('wa-address').value.trim(),
|
||||
email: document.getElementById('wa-email').value.trim(),
|
||||
vertical: document.getElementById('wa-vertical').value,
|
||||
websites: [document.getElementById('wa-web1').value.trim(),
|
||||
document.getElementById('wa-web2').value.trim()].filter(Boolean),
|
||||
};
|
||||
try {
|
||||
const res = await fetch(API + 'wa_profile.php', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) { toast(json.error || 'Error', 'error'); return; }
|
||||
toast('Perfil de WhatsApp actualizado');
|
||||
document.getElementById('wa-perfil-about').textContent = body.about || '—';
|
||||
} catch (e) { toast('Error de conexión', 'error'); }
|
||||
}
|
||||
|
||||
async function subirFotoPerfil(input) {
|
||||
if (!input.files[0]) return;
|
||||
const form = new FormData();
|
||||
form.append('accion', 'foto');
|
||||
form.append('foto', input.files[0]);
|
||||
const btn = input.closest('label');
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
try {
|
||||
const res = await fetch(API + 'wa_profile.php', { method: 'POST', body: form });
|
||||
const json = await res.json();
|
||||
if (!json.ok) { toast(json.error || 'Error al subir foto', 'error'); return; }
|
||||
// Preview local
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
document.getElementById('wa-perfil-foto').src = e.target.result;
|
||||
document.getElementById('wa-perfil-foto').style.display = '';
|
||||
document.getElementById('wa-perfil-foto-placeholder').style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
toast('Foto de perfil actualizada');
|
||||
} catch (e) { toast('Error al subir foto', 'error'); } finally {
|
||||
btn.innerHTML = orig;
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function guardarWhatsApp() {
|
||||
const template = document.getElementById('wa-template')?.value.trim();
|
||||
const templateMuestra = document.getElementById('wa-template-muestra')?.value.trim();
|
||||
|
||||
Reference in New Issue
Block a user