feat: soporte de imagen dinámica en plantillas WhatsApp
- Detecta header_type=IMAGE en get_template_details.php
- send_message (individual): muestra input de imagen si la plantilla lo requiere,
sube a upload_media.php y envía header_image_url al backend
- broadcast (masivo): igual, con upload previo al envío
- api/send_message.php: acepta header_image_url y lo pasa como headerParameters
[type:image, image:{link:URL}] a WhatsAppService::sendTemplateMessage()
- api/send_broadcast.php: igual para cada destinatario del masivo
- Preview en tiempo real de la imagen seleccionada en ambos formularios
This commit is contained in:
+12
-1
@@ -168,11 +168,22 @@ try {
|
|||||||
throw new Exception("La plantilla requiere {$expectedVars} variable(s) numéricas, pero se enviaron " . count($processedVariables));
|
throw new Exception("La plantilla requiere {$expectedVars} variable(s) numéricas, pero se enviaron " . count($processedVariables));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preparar header de imagen dinámica si se proporcionó
|
||||||
|
$headerParams = [];
|
||||||
|
if (!empty($input['header_image_url'])) {
|
||||||
|
$imageUrl = filter_var($input['header_image_url'], FILTER_VALIDATE_URL) ? $input['header_image_url'] : null;
|
||||||
|
if ($imageUrl) {
|
||||||
|
$headerParams = [['type' => 'image', 'image' => ['link' => $imageUrl]]];
|
||||||
|
error_log("Broadcast template header image URL: " . $imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$response = $whatsappService->sendTemplateMessage(
|
$response = $whatsappService->sendTemplateMessage(
|
||||||
$user['phone_number'],
|
$user['phone_number'],
|
||||||
$template['template_name'] ?? $template['name'],
|
$template['template_name'] ?? $template['name'],
|
||||||
$template['language_code'] ?? 'es',
|
$template['language_code'] ?? 'es',
|
||||||
$processedVariables
|
$processedVariables,
|
||||||
|
$headerParams
|
||||||
);
|
);
|
||||||
|
|
||||||
// Construir contenido para BD (reemplazar variables en body_text)
|
// Construir contenido para BD (reemplazar variables en body_text)
|
||||||
|
|||||||
+11
-1
@@ -260,9 +260,19 @@ try {
|
|||||||
error_log("Method: sendTemplateMessage()");
|
error_log("Method: sendTemplateMessage()");
|
||||||
error_log("========================================================");
|
error_log("========================================================");
|
||||||
|
|
||||||
|
// Preparar header de imagen dinámica si se proporcionó
|
||||||
|
$headerParams = [];
|
||||||
|
if (!empty($input['header_image_url'])) {
|
||||||
|
$imageUrl = filter_var($input['header_image_url'], FILTER_VALIDATE_URL) ? $input['header_image_url'] : null;
|
||||||
|
if ($imageUrl) {
|
||||||
|
$headerParams = [['type' => 'image', 'image' => ['link' => $imageUrl]]];
|
||||||
|
error_log("Template header image URL: " . $imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Intentar envío y, si falla por traducción no encontrada, probar variantes de idioma
|
// Intentar envío y, si falla por traducción no encontrada, probar variantes de idioma
|
||||||
try {
|
try {
|
||||||
$response = $whatsappService->sendTemplateMessage($recipient, $template, $language, $parameters, [], $componentsSimple);
|
$response = $whatsappService->sendTemplateMessage($recipient, $template, $language, $parameters, $headerParams, $componentsSimple);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
|||||||
+83
-12
@@ -1558,11 +1558,25 @@ class SimpleWhatsAppManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subir imagen de header si el usuario seleccionó una
|
||||||
|
let headerImageUrl = null;
|
||||||
|
const msgImgInput = document.getElementById('message-template-image');
|
||||||
|
if (msgImgInput && msgImgInput.files && msgImgInput.files[0]) {
|
||||||
|
try {
|
||||||
|
this.showInfo('Subiendo imagen de la plantilla...');
|
||||||
|
headerImageUrl = await this._uploadTemplateImage(msgImgInput);
|
||||||
|
} catch (e) {
|
||||||
|
this.showError('Error subiendo imagen de la plantilla: ' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
messageData = {
|
messageData = {
|
||||||
recipient: recipient,
|
recipient: recipient,
|
||||||
type: 'template',
|
type: 'template',
|
||||||
template_name: templateName, // Usar template_name en lugar de ID
|
template_name: templateName, // Usar template_name en lugar de ID
|
||||||
parameters: tplParams
|
parameters: tplParams,
|
||||||
|
...(headerImageUrl ? { header_image_url: headerImageUrl } : {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1935,6 +1949,18 @@ class SimpleWhatsAppManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subir imagen de header broadcast si existe
|
||||||
|
const bcImgInput = document.getElementById('broadcast-template-image');
|
||||||
|
if (bcImgInput && bcImgInput.files && bcImgInput.files[0]) {
|
||||||
|
try {
|
||||||
|
this.showInfo('Subiendo imagen de la plantilla...');
|
||||||
|
payload.header_image_url = await this._uploadTemplateImage(bcImgInput);
|
||||||
|
} catch (e) {
|
||||||
|
this.showError('Error subiendo imagen de la plantilla: ' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.showInfo('Enviando mensajes masivos...');
|
this.showInfo('Enviando mensajes masivos...');
|
||||||
const result = await this.apiCall('send_broadcast.php', {
|
const result = await this.apiCall('send_broadcast.php', {
|
||||||
@@ -1956,6 +1982,19 @@ class SimpleWhatsAppManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sube una imagen al servidor y retorna la URL pública
|
||||||
|
async _uploadTemplateImage(fileInput) {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return null;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', file);
|
||||||
|
const resp = await fetch('./api/upload_media.php', { method: 'POST', body: fd });
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && data.public_url) return data.public_url;
|
||||||
|
throw new Error(data.error || 'No se pudo subir la imagen');
|
||||||
|
}
|
||||||
|
|
||||||
// Gestión de usuarios centralizada en lab_usuarios.php
|
// Gestión de usuarios centralizada en lab_usuarios.php
|
||||||
async loadAdminUsers() {
|
async loadAdminUsers() {
|
||||||
// No-op: la UI redirige a lab_usuarios.php
|
// No-op: la UI redirige a lab_usuarios.php
|
||||||
@@ -3184,22 +3223,53 @@ window.loadMessageTemplateDetails = async function() {
|
|||||||
console.log('📋 Variables:', template.variables);
|
console.log('📋 Variables:', template.variables);
|
||||||
|
|
||||||
const variablesContainer = document.getElementById('message-template-variables');
|
const variablesContainer = document.getElementById('message-template-variables');
|
||||||
|
|
||||||
|
// Detectar header de imagen
|
||||||
|
const hasImageHeader = (template.header_type || '').toUpperCase() === 'IMAGE';
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (hasImageHeader) {
|
||||||
|
html += `
|
||||||
|
<div class="mb-3 border rounded p-2 bg-light" id="msg-image-header-section">
|
||||||
|
<label class="form-label fw-bold text-primary"><i class="fas fa-image me-1"></i> Imagen del encabezado</label>
|
||||||
|
<p class="text-muted small mb-2">Esta plantilla requiere una imagen de encabezado dinámica. Súbela aquí.</p>
|
||||||
|
<input type="file" class="form-control form-control-sm" id="message-template-image" accept="image/jpeg,image/png,image/webp" required>
|
||||||
|
<div id="msg-image-preview" class="mt-2" style="display:none">
|
||||||
|
<img id="msg-image-thumb" src="" style="max-height:100px;border-radius:6px;" class="border">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
setTimeout(() => {
|
||||||
|
const imgInput = document.getElementById('message-template-image');
|
||||||
|
if (imgInput) imgInput.addEventListener('change', function() {
|
||||||
|
const file = this.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('msg-image-thumb').src = e.target.result;
|
||||||
|
document.getElementById('msg-image-preview').style.display = 'block';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
if (template.variables && template.variables.length > 0) {
|
if (template.variables && template.variables.length > 0) {
|
||||||
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
||||||
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
|
html += `<label class="form-label fw-bold${hasImageHeader ? ' mt-2' : ''}"><i class="fas fa-edit"></i> Variables de la Plantilla</label>`;
|
||||||
|
|
||||||
template.variables.forEach((variable, index) => {
|
template.variables.forEach((variable, index) => {
|
||||||
const varLabel = variable.label || variable.name || `Variable ${variable.index || index + 1}`;
|
const varLabel = variable.label || variable.name || `Variable ${variable.index || index + 1}`;
|
||||||
const placeholder = variable.placeholder || `{{${variable.index || index + 1}}}`;
|
const placeholder = variable.placeholder || `{{${variable.index || index + 1}}}`;
|
||||||
const example = variable.example || '';
|
const example = variable.example || '';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small text-muted">${varLabel}</label>
|
<label class="form-label small text-muted">${varLabel}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control form-control-sm message-template-var"
|
class="form-control form-control-sm message-template-var"
|
||||||
data-var="${variable.index || index + 1}"
|
data-var="${variable.index || index + 1}"
|
||||||
data-placeholder="${placeholder}"
|
data-placeholder="${placeholder}"
|
||||||
placeholder="${example || 'Ingrese ' + varLabel.toLowerCase()}"
|
placeholder="${example || 'Ingrese ' + varLabel.toLowerCase()}"
|
||||||
@@ -3208,15 +3278,16 @@ window.loadMessageTemplateDetails = async function() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('📝 HTML generado para variables:', html);
|
console.log('📝 HTML generado para variables:', html);
|
||||||
variablesContainer.innerHTML = html;
|
variablesContainer.innerHTML = html;
|
||||||
variablesContainer.style.display = 'block';
|
variablesContainer.style.display = 'block';
|
||||||
console.log('✅ Variables container mostrado');
|
console.log('✅ Variables container mostrado');
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ Template sin variables');
|
if (!hasImageHeader) html += '<p class="text-muted small">Esta plantilla no tiene variables de texto</p>';
|
||||||
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
|
console.log('⚠️ Template sin variables de texto');
|
||||||
variablesContainer.style.display = 'block';
|
variablesContainer.innerHTML = html;
|
||||||
|
variablesContainer.style.display = (hasImageHeader || html) ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMessagePreview();
|
updateMessagePreview();
|
||||||
|
|||||||
@@ -1977,22 +1977,53 @@ try {
|
|||||||
console.log('📋 Variables:', template.variables);
|
console.log('📋 Variables:', template.variables);
|
||||||
|
|
||||||
const variablesContainer = document.getElementById('broadcast-template-variables');
|
const variablesContainer = document.getElementById('broadcast-template-variables');
|
||||||
|
|
||||||
|
// Detectar header de imagen
|
||||||
|
const hasImageHeader = (template.header_type || '').toUpperCase() === 'IMAGE';
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (hasImageHeader) {
|
||||||
|
html += `
|
||||||
|
<div class="mb-3 border rounded p-2 bg-light" id="bc-image-header-section">
|
||||||
|
<label class="form-label fw-bold text-primary"><i class="fas fa-image me-1"></i> Imagen del encabezado</label>
|
||||||
|
<p class="text-muted small mb-2">Esta plantilla requiere una imagen de encabezado dinámica. Súbela aquí.</p>
|
||||||
|
<input type="file" class="form-control" id="broadcast-template-image" accept="image/jpeg,image/png,image/webp" required>
|
||||||
|
<div id="bc-image-preview" class="mt-2" style="display:none">
|
||||||
|
<img id="bc-image-thumb" src="" style="max-height:100px;border-radius:6px;" class="border">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
setTimeout(() => {
|
||||||
|
const imgInput = document.getElementById('broadcast-template-image');
|
||||||
|
if (imgInput) imgInput.addEventListener('change', function() {
|
||||||
|
const file = this.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('bc-image-thumb').src = e.target.result;
|
||||||
|
document.getElementById('bc-image-preview').style.display = 'block';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
if (template.variables && template.variables.length > 0) {
|
if (template.variables && template.variables.length > 0) {
|
||||||
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
console.log(`✅ Se encontraron ${template.variables.length} variables`);
|
||||||
let html = '<label class="form-label fw-bold"><i class="fas fa-edit"></i> Variables de la Plantilla</label>';
|
html += `<label class="form-label fw-bold${hasImageHeader ? ' mt-2' : ''}"><i class="fas fa-edit"></i> Variables de la Plantilla</label>`;
|
||||||
|
|
||||||
template.variables.forEach((variable, index) => {
|
template.variables.forEach((variable, index) => {
|
||||||
console.log(`Variable ${index}:`, variable);
|
console.log(`Variable ${index}:`, variable);
|
||||||
const placeholder = variable.example || `Valor para Variable ${variable.index}`;
|
const placeholder = variable.example || `Valor para Variable ${variable.index}`;
|
||||||
const label = variable.label || `Variable ${variable.index}`;
|
const label = variable.label || `Variable ${variable.index}`;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label small">${label}</label>
|
<label class="form-label small">${label}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control broadcast-template-var"
|
class="form-control broadcast-template-var"
|
||||||
data-var="${variable.index}"
|
data-var="${variable.index}"
|
||||||
data-placeholder="${variable.placeholder}"
|
data-placeholder="${variable.placeholder}"
|
||||||
placeholder="${placeholder}"
|
placeholder="${placeholder}"
|
||||||
@@ -2001,15 +2032,16 @@ try {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('📝 HTML generado para variables:', html);
|
console.log('📝 HTML generado para variables:', html);
|
||||||
variablesContainer.innerHTML = html;
|
variablesContainer.innerHTML = html;
|
||||||
variablesContainer.style.display = 'block';
|
variablesContainer.style.display = 'block';
|
||||||
console.log('✅ Variables container mostrado');
|
console.log('✅ Variables container mostrado');
|
||||||
} else {
|
} else {
|
||||||
|
if (!hasImageHeader) html += '<p class="text-muted small">Esta plantilla no tiene variables de texto</p>';
|
||||||
console.log('⚠️ Template sin variables');
|
console.log('⚠️ Template sin variables');
|
||||||
variablesContainer.innerHTML = '<p class="text-muted small">Esta plantilla no tiene variables</p>';
|
variablesContainer.innerHTML = html;
|
||||||
variablesContainer.style.display = 'block';
|
variablesContainer.style.display = (hasImageHeader || html) ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBroadcastPreview();
|
updateBroadcastPreview();
|
||||||
|
|||||||
Reference in New Issue
Block a user