antes del cambio de color de los botones

This commit is contained in:
DANYDHSV 2026-03-10 17:42:34 -06:00
parent d0430dd891
commit e113c750b9
6 changed files with 523 additions and 15909 deletions

View File

@ -1,5 +1,16 @@
# CHANGELOG - SIIP WhatsApp Notifications Plugin # CHANGELOG - SIIP WhatsApp Notifications Plugin
## VERSIÓN 4.4.0 - 10-03-2026
### ✨ Nuevas Características (Features)
1**Reenvío manual de notificaciones de tareas**: Nuevo módulo en la sección "Gestión de Instaladores" que permite ver las tareas activas ("En curso") de cada técnico y reenviar manualmente la notificación WhatsApp de asignación con un solo clic.
- **Nuevo endpoint `GET ?action=get_installer_jobs`**: Consulta la API de UCRM para obtener los jobs activos (`status=1`) filtrados por instalador, enriquecidos con nombre del cliente.
- **Nuevo endpoint `POST action=resend_job_notification`**: Simula un webhook `job.edit` (status 0→1) vía loopback curl para disparar el flujo completo de notificación.
- **Tabla interactiva**: Card "📋 Tareas Activas del Instalador" con columnas Folio, Cliente, Fecha, Descripción y botón 📨 Reenviar.
- **Icono 📋 en tabla de instaladores**: Permite cargar las tareas activas de cualquier técnico con un clic.
## VERSIÓN 4.3.1 - 10-03-2026 ## VERSIÓN 4.3.1 - 10-03-2026
### 🐛 Correcciones (Bug Fixes) ### 🐛 Correcciones (Bug Fixes)

View File

@ -1,12 +1,18 @@
# SIIP - WhatsApp Notifications & Integrated Payment Portal # SIIP - WhatsApp Notifications & Integrated Payment Portal
![Version](https://img.shields.io/badge/version-4.3.1-blue.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/version-4.4.0-blue.svg?style=for-the-badge)
![UCRM Compatibility](https://img.shields.io/badge/UCRM-v2.1.0%2B-green.svg?style=for-the-badge) ![UCRM Compatibility](https://img.shields.io/badge/UCRM-v2.1.0%2B-green.svg?style=for-the-badge)
![Status](https://img.shields.io/badge/status-PRODUCTION-success.svg?style=for-the-badge) ![Status](https://img.shields.io/badge/status-PRODUCTION-success.svg?style=for-the-badge)
![Author](https://img.shields.io/badge/author-SIIP_INTERNET-orange.svg?style=for-the-badge) ![Author](https://img.shields.io/badge/author-SIIP_INTERNET-orange.svg?style=for-the-badge)
Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos. Este plugin es una solución integral que transforma tu UCRM en un **Portal Administrativo de Última Generación**. No solo automatiza la comunicación por WhatsApp, sino que integra un Dashboard completo para la gestión de pagos online (Stripe/OXXO), visualización de comprobantes y coordinación de equipos técnicos.
## ✨ Novedades v4.4.0 (Resend Job Notifications)
- **📋 Tabla de Tareas Activas por Instalador**: Nuevo módulo dentro de "Gestión de Instaladores" que muestra los jobs "En curso" de cada técnico con datos de cliente, fecha y descripción.
- **📨 Reenvío Manual de Notificaciones**: Botón para reenviar la notificación WhatsApp de asignación de tarea a cualquier instalador desde la interfaz del plugin.
- **🔗 Integración con API de Scheduling**: Consulta dinámica de `GET /scheduling/jobs?assignedUserId=X&statuses[]=1` para listar tareas activas en tiempo real.
## 🐛 Hotfix v4.3.1 (Installer Notification Fix) ## 🐛 Hotfix v4.3.1 (Installer Notification Fix)
- **🔧 Fix Cambio de Instalador**: Corregido bug donde el nuevo técnico recibía mensaje de desasignación en vez de asignación al cambiar instalador en una tarea "En curso". - **🔧 Fix Cambio de Instalador**: Corregido bug donde el nuevo técnico recibía mensaje de desasignación en vez de asignación al cambiar instalador en una tarea "En curso".

View File

@ -1 +1,35 @@
{"ipserver":"venus.siip.mx","apitoken":"gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9","unmsApiToken":"4f5219de-cc5b-413d-b2fb-5133d02f3b26","tokencallbell":"g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79","tokenstripe":"sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M","hostServerFTP":"siip.mx","usernameServerFTP":"siip0001","passServerFTP":"$spGiT,[wa)n","ipPuppeteer":"172.16.5.134","portPuppeteer":"4100","idPaymentAdminCRM":"1180","cashPaymentMethodId":true,"courtesyPaymentMethodId":true,"bankTransferPaymentMethodId":true,"paypalPaymentMethodId":true,"creditCardPaypalPaymentMethodId":true,"creditCardStripePaymentMethodId":true,"stripeSubscriptionCreditCardPaymentMethodId":true,"paypalSubscriptionPaymentMethodId":true,"mercadopagoPaymentMethodId":true,"checkPaymentMethodId":true,"customPaymentMethodId":true,"oxxoPayPaymentMethodId":true,"creditDebitCardPaymentMethodId":true,"notificationTypeText":false,"installersDataWhatsApp":"{\"instaladores\":[{\"id\":1019,\"nombre\":\"Mucio Robledo\",\"whatsapp\":\"4181878106\"},{\"id\":1173,\"nombre\":\"Ángel Arvizu\",\"whatsapp\":\"4181878106\"},{\"id\":1172,\"nombre\":\"Juan Rostro\",\"whatsapp\":\"4181878106\"},{\"id\":1015,\"nombre\":\"Daniel Humberto\",\"whatsapp\":\"4181878106\"},{\"id\":1131,\"nombre\":\"Gricelda Avalos\",\"whatsapp\":\"4181817609\"}]}","debugMode":true,"minioEndpoint":"http://172.16.5.134:9002","minioPublicUrl":"https://aws-venus.siip.mx","minioAccessKey":"minioadmin","minioSecretKey":"minioadmin","minioBucket":"vouchers-oxxo","logging_level":true} {
"ipserver": "venus.siip.mx",
"apitoken": "gvcnIJqXdUjneVSjhl6THLlQcYXJyIFCcwHKVba2bvIrNraanCTb5VeoWuJ0TFZ9",
"unmsApiToken": "4f5219de-cc5b-413d-b2fb-5133d02f3b26",
"tokencallbell": "g8thcZkXGd3xBj2g3TtYNYFMH1fuesbJ.b6a940ea7d78cf6c9e42f067b21c8ddf96e9fa2a9e307bfd0c7c7c4d7fa38f79",
"tokenstripe": "sk_test_51OkG0REFY1WEUtgRH6UxBK5pu80Aq5Iy8EcdPnf0cOWzuVLQTpyLCd7CbPzqMsWMafZOHElCxhEHF7g8boURjWlJ00tBwE0W1M",
"hostServerFTP": "siip.mx",
"usernameServerFTP": "siip0001",
"passServerFTP": "$spGiT,[wa)n",
"ipPuppeteer": "172.16.5.134",
"portPuppeteer": "4100",
"idPaymentAdminCRM": "1180",
"cashPaymentMethodId": true,
"courtesyPaymentMethodId": true,
"bankTransferPaymentMethodId": true,
"paypalPaymentMethodId": true,
"creditCardPaypalPaymentMethodId": true,
"creditCardStripePaymentMethodId": true,
"stripeSubscriptionCreditCardPaymentMethodId": true,
"paypalSubscriptionPaymentMethodId": true,
"mercadopagoPaymentMethodId": true,
"checkPaymentMethodId": true,
"customPaymentMethodId": true,
"oxxoPayPaymentMethodId": true,
"creditDebitCardPaymentMethodId": true,
"notificationTypeText": false,
"installersDataWhatsApp": "{\"instaladores\":[{\"id\":1019,\"nombre\":\"Mucio Robledo\",\"whatsapp\":\"4181878106\"},{\"id\":1173,\"nombre\":\"Ángel Arvizu\",\"whatsapp\":\"4181878106\"},{\"id\":1172,\"nombre\":\"Juan Rostro\",\"whatsapp\":\"4181878106\"},{\"id\":1015,\"nombre\":\"Daniel Humberto\",\"whatsapp\":\"4181878106\"},{\"id\":1131,\"nombre\":\"Gricelda Avalos\",\"whatsapp\":\"4181817609\"},{\"id\":\"1184\",\"nombre\":\"José Luis Enrique Sánchez\",\"whatsapp\":\"4181878106\"}]}",
"debugMode": true,
"minioEndpoint": "http:\/\/172.16.5.134:9002",
"minioPublicUrl": "https:\/\/aws-venus.siip.mx",
"minioAccessKey": "minioadmin",
"minioSecretKey": "minioadmin",
"minioBucket": "vouchers-oxxo",
"logging_level": true
}

File diff suppressed because one or more lines are too long

View File

@ -5,13 +5,18 @@
"displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp", "displayName": "SIIP - Procesador de Pagos en línea con Stripe, Oxxo y Transferencia, Sincronizador de CallBell y Envío de Notificaciones y comprobantes vía WhatsApp",
"description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes", "description": "Este plugin sincroniza los clientes del sistema UISP CRM con los contactos de WhatsApp en CallBell, además procesa pagos de Stripe como las trasferencias bancarias y genera referencias de pago vía OXXO, además envía comprobantes de pago en formato imagen PNG o texto vía Whatsapp a los clientes",
"url": "https://siip.mx/", "url": "https://siip.mx/",
"version": "4.3.1", "version": "4.4.0",
"unmsVersionCompliancy": { "unmsVersionCompliancy": {
"min": "2.1.0", "min": "2.1.0",
"max": null "max": null
}, },
"author": "SIIP INTERNET", "author": "SIIP INTERNET",
"changelog": [ "changelog": [
{
"version": "4.4.0",
"date": "2026-03-10",
"changes": "Nueva funcionalidad: Reenvío manual de notificaciones de tareas a instaladores desde el módulo Gestión de Instaladores. Tabla de jobs activos por técnico con botón de reenvío."
},
{ {
"version": "4.3.1", "version": "4.3.1",
"date": "2026-03-10", "date": "2026-03-10",

View File

@ -236,6 +236,60 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
} }
exit; exit;
} }
if ($_POST['action'] === 'resend_job_notification') {
$jobId = $_POST['jobId'] ?? null;
if (!$jobId) {
echo json_encode(['success' => false, 'message' => 'jobId requerido']);
exit;
}
try {
$job = $ucrmApi->get("scheduling/jobs/$jobId");
$client = $ucrmApi->get("clients/{$job['clientId']}");
// Simular webhook job.edit con activación (status 0→1)
$entityBeforeEdit = $job;
$entityBeforeEdit['status'] = 0; // Simular que estaba en "Abierto"
$entity = $job;
$entity['status'] = 1; // Estado actual: "En curso"
// Agregar prefijo para que verifyJobActionToDo reconozca que debe notificar
$entity['title'] = '[NOTIFICACION-PENDIENTE]' . ($entity['title'] ?? '');
$payload = [
'uuid' => 'manual-trigger',
'changeType' => 'update',
'entity' => 'job',
'entityId' => (int)$jobId,
'eventName' => 'job.edit',
'extraData' => [
'entity' => $entity,
'entityBeforeEdit' => $entityBeforeEdit
]
];
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$selfUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];
$ch = curl_init($selfUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POSTREDIR, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$res = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
echo json_encode(['success' => true, 'message' => 'Notificación reenviada para job #' . $jobId]);
} catch (\Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
}
} }
if (isset($_GET['action'])) { if (isset($_GET['action'])) {
@ -268,6 +322,45 @@ if (isset($_GET['action'])) {
exit; exit;
} }
if ($_GET['action'] === 'get_installer_jobs') {
$installerId = $_GET['installerId'] ?? null;
if (!$installerId) {
echo json_encode([]);
exit;
}
try {
$jobs = $ucrmApi->get('scheduling/jobs', [
'assignedUserId' => $installerId,
'statuses[]' => 1,
'limit' => 50
]);
$result = [];
foreach ($jobs as $job) {
$clientName = 'Sin cliente';
if (!empty($job['clientId'])) {
try {
$client = $ucrmApi->get("clients/{$job['clientId']}");
$clientName = trim(($client['firstName'] ?? '') . ' ' . ($client['lastName'] ?? ''));
} catch (\Exception $e) {
$clientName = 'Cliente #' . $job['clientId'];
}
}
$result[] = [
'id' => $job['id'],
'title' => $job['title'] ?? 'Sin título',
'date' => $job['date'] ?? '',
'clientName' => $clientName,
'clientId' => $job['clientId'] ?? null,
'description' => mb_substr($job['description'] ?? '', 0, 80)
];
}
echo json_encode($result);
} catch (\Exception $e) {
echo json_encode([]);
}
exit;
}
if ($_GET['action'] === 'image' || $_GET['action'] === 'get_image') { if ($_GET['action'] === 'image' || $_GET['action'] === 'get_image') {
// Image Handler // Image Handler
if (ob_get_level()) ob_end_clean(); if (ob_get_level()) ob_end_clean();
@ -1717,6 +1810,46 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<p>No se encontraron instaladores</p> <p>No se encontraron instaladores</p>
</div> </div>
</div> </div>
<!-- JOBS ACTIVOS DEL INSTALADOR -->
<div class="card" style="margin-top: 2rem;">
<div style="margin-bottom: 1.5rem;">
<h2 id="jobsSectionTitle" style="margin: 0; display: flex; align-items: center; gap: 10px;">
📋 Tareas Activas del Instalador
</h2>
<p style="color: var(--text-muted); margin: 5px 0 0 0;">Selecciona un instalador con el botón 📋 para ver sus tareas "En curso" y reenviar notificaciones</p>
</div>
<div id="jobsEmptyState" class="placeholder-state">
<span class="placeholder-icon">👷</span>
<p>Selecciona un instalador para ver sus tareas activas</p>
</div>
<div id="jobsLoading" style="display: none; text-align: center; padding: 2rem; color: var(--text-muted);">
<div class="loader" style="margin: 0 auto 1rem;"></div>
<p>Cargando tareas...</p>
</div>
<div id="jobsTableWrapper" style="display: none; overflow-x: auto;">
<table id="installerJobsTable">
<thead>
<tr>
<th>Folio</th>
<th>Cliente</th>
<th>Fecha</th>
<th>Descripción</th>
<th>Acción</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="jobsNoResults" class="placeholder-state" style="display: none;">
<span class="placeholder-icon"></span>
<p>Este instalador no tiene tareas "En curso"</p>
</div>
</div>
</section> </section>
<!-- MODULE 2: NOTIFICATIONS --> <!-- MODULE 2: NOTIFICATIONS -->
@ -2659,6 +2792,7 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
<td>${inst.nombre}</td> <td>${inst.nombre}</td>
<td>${inst.whatsapp}</td> <td>${inst.whatsapp}</td>
<td> <td>
<span style="cursor:pointer; font-size:1.3em; margin-right:8px;" onclick="loadInstallerJobs('${inst.id}', '${inst.nombre}')" title="Ver tareas activas">📋</span>
<img src="?action=image&file=edit.webp" class="icon-action" onclick="editInstaller(${i})" title="Editar"> <img src="?action=image&file=edit.webp" class="icon-action" onclick="editInstaller(${i})" title="Editar">
<img src="?action=image&file=delete.webp" class="icon-action" style="margin-left:10px" onclick="deleteInstaller(${i})" title="Borrar"> <img src="?action=image&file=delete.webp" class="icon-action" style="margin-left:10px" onclick="deleteInstaller(${i})" title="Borrar">
</td> </td>
@ -2703,6 +2837,104 @@ $installersData = json_decode($config['installersDataWhatsApp'] ?? '{"instalador
} }
} }
// --- JOBS ACTIVOS DEL INSTALADOR ---
async function loadInstallerJobs(installerId, installerName) {
const title = document.getElementById('jobsSectionTitle');
const emptyState = document.getElementById('jobsEmptyState');
const loading = document.getElementById('jobsLoading');
const tableWrapper = document.getElementById('jobsTableWrapper');
const noResults = document.getElementById('jobsNoResults');
title.innerHTML = `📋 Tareas Activas: <strong>${installerName}</strong>`;
emptyState.style.display = 'none';
loading.style.display = 'block';
tableWrapper.style.display = 'none';
noResults.style.display = 'none';
// Scroll suave a la sección de jobs
title.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
try {
const resp = await fetch(`?action=get_installer_jobs&installerId=${installerId}`);
const jobs = await resp.json();
loading.style.display = 'none';
if (!jobs.length) {
noResults.style.display = 'block';
return;
}
const tbody = document.querySelector('#installerJobsTable tbody');
tbody.innerHTML = jobs.map(job => {
const dateFormatted = job.date ? new Date(job.date).toLocaleDateString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
}) : 'S/F';
const titleClean = job.title.replace('[NOTIFICACION-PENDIENTE]', '').replace('[CLIENTE-SIN-WHATSAPP]', '').trim();
return `<tr>
<td><strong>#${job.id}</strong></td>
<td>[${job.clientId}] ${job.clientName}</td>
<td>${dateFormatted}</td>
<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${job.description}">${titleClean || job.description || 'Sin descripción'}</td>
<td>
<button class="btn btn-primary" style="padding:6px 14px; font-size:0.85em;" onclick="resendJobNotification(${job.id}, this)">
📨 Reenviar
</button>
</td>
</tr>`;
}).join('');
tableWrapper.style.display = 'block';
} catch (e) {
loading.style.display = 'none';
noResults.style.display = 'block';
console.error('Error loading installer jobs:', e);
}
}
async function resendJobNotification(jobId, btn) {
const originalText = btn.innerHTML;
btn.innerHTML = '⏳ Enviando...';
btn.disabled = true;
try {
const fd = new FormData();
fd.append('action', 'resend_job_notification');
fd.append('jobId', jobId);
const resp = await fetch('', {
method: 'POST',
body: fd
});
const result = await resp.json();
if (result.success) {
btn.innerHTML = '✅ Enviado';
btn.style.backgroundColor = '#22c55e';
showToast('Notificación reenviada para tarea #' + jobId);
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.backgroundColor = '';
btn.disabled = false;
}, 3000);
} else {
throw new Error(result.message || 'Error desconocido');
}
} catch (e) {
btn.innerHTML = '❌ Error';
btn.style.backgroundColor = '#ef4444';
showToast('Error: ' + e.message, true);
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.backgroundColor = '';
btn.disabled = false;
}, 3000);
}
}
function openInstallerModal() { function openInstallerModal() {
document.getElementById('modalOverlay').style.display = 'flex'; document.getElementById('modalOverlay').style.display = 'flex';
document.getElementById('installerForm').reset(); document.getElementById('installerForm').reset();