- Reemplazo de comillas simples por backticks (literales de plantilla) en strings multilínea dentro de `views/stripe.php` para evitar errores fatales de JavaScript que bloqueaban la navegación del menú y la interacción del usuario. - Mejora de la lógica en la interfaz para manejar el estado de pago "processing" (común en transferencias SPEI), mostrando el mensaje "Pago en proceso" con instrucciones claras en lugar del mensaje genérico de error. - Corrección del objeto global `store` en `public.php` eliminando propiedades duplicadas que podían afectar la persistencia de la sesión. - Actualización de versión a 4.6.1 en CHANGELOG, README y manifest.json.
318 lines
18 KiB
PHP
Executable File
318 lines
18 KiB
PHP
Executable File
<?php if (!isset($isModuleJs)): ?>
|
|
<!-- MODULE 3: STRIPE -->
|
|
<section id="section-pagos-spei" class="section-view">
|
|
<div class="card">
|
|
<div style="margin-bottom: 2rem;">
|
|
<h2 style="margin: 0; display: flex; align-items: center; gap: 10px;">
|
|
<img src="?action=get_image&name=online-payments-stripe.png" style="width:64px;height:64px;"> Pagos SPEI
|
|
</h2>
|
|
<p style="color: var(--text-muted); margin: 5px 0 0 0;">🏦 Genera referencias de transferencia bancaria para que tus clientes paguen vía SPEI</p>
|
|
</div>
|
|
|
|
<!-- CONFIG CONTAINER -->
|
|
<div class="config-container">
|
|
<div class="form-group" style="position: relative;">
|
|
<label>Buscar Cliente para Stripe</label>
|
|
<div class="search-wrapper">
|
|
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="11" cy="11" r="8" />
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
</svg>
|
|
<input type="text" id="stripeSearch" class="form-control search-input-padded" placeholder="Buscar cliente..." autocomplete="off">
|
|
</div>
|
|
<div id="stripeResults" class="search-results"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PLACEHOLDER STATE (Initially Visible) -->
|
|
<div id="stripePlaceholder" class="placeholder-state">
|
|
<span class="placeholder-icon">🏦</span>
|
|
<p>Selecciona un cliente para generar una referencia de pago SPEI</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="stripeDetailContainer" class="card" style="display: none;">
|
|
<!-- HEADER: CLIENT INFO & BALANCE -->
|
|
<div class="client-header-grid">
|
|
<div class="client-info">
|
|
<h3>
|
|
<img src="?action=image&file=client.webp" class="client-header-icon"> <span id="stripeClientName">Nombre del Cliente</span>
|
|
</h3>
|
|
<p style="color: var(--text-muted); margin: 0 0 3px 0;">
|
|
<span id="stripeClientIdDisplay">ID: #0</span>
|
|
</p>
|
|
<p id="stripeClientAddressDisplay" style="color: var(--text-muted); margin: 0 0 12px 0; font-size: 0.82rem; display: none;"></p>
|
|
<!-- Botón Ver en CRM debajo del ID y domicilio -->
|
|
<a id="btnVerEnCrm" href="#" target="_blank" style="display:inline-flex;align-items:center;gap:9px;padding:7px 16px;background:rgba(255,255,255,0.05);border:1px solid var(--border);border-radius:9px;color:var(--text-main);text-decoration:none;font-size:0.85rem;font-weight:500;transition:background 0.2s;">
|
|
<img src="?action=image&file=crm.webp" style="width:30px;height:30px;">
|
|
Ver en CRM
|
|
</a>
|
|
</div>
|
|
<div class="client-balance">
|
|
<span class="label">Saldo en CRM</span>
|
|
<span class="badge" id="stripeBalanceBadge">$0.00</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TECHNICAL DETAILS GRID -->
|
|
<div class="details-grid">
|
|
<div class="detail-item">
|
|
<label>Stripe Customer ID</label>
|
|
<div class="value-box" id="stripeCustomerIdDisplay">-</div>
|
|
</div>
|
|
<div class="detail-item">
|
|
<label>Clabe Interbancaria</label>
|
|
<div class="value-box" id="stripeClabeDisplay">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PAYMENT INTENT FORM -->
|
|
<div>
|
|
<h4 class="section-title">Generar Intención de Pago</h4>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label>Monto a Cobrar</label>
|
|
<div class="input-group-unified">
|
|
<span class="input-prefix">$</span>
|
|
<input type="number" id="stripeAmount" class="form-control" placeholder="0.00">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Asignar a Administrador</label>
|
|
<select id="stripeAdminSelect" class="form-control">
|
|
<option value="">-- Automático (Sistema) --</option>
|
|
<?php /** @var array{id: int, nombre: string}[] $admins */
|
|
foreach ($admins as $admin): echo "<option value='{$admin['id']}'>{$admin['nombre']}</option>";
|
|
endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botón CTA centrado y destacado -->
|
|
<div style="text-align:center; margin-top:1.8rem;">
|
|
<button id="btnCreateIntent" style="display:inline-flex;align-items:center;gap:10px;padding:15px 52px;font-size:1.05rem;font-weight:700;border:none;border-radius:14px;cursor:pointer;background:linear-gradient(135deg,#6c63ff,#4f46e5);color:#fff;box-shadow:0 6px 24px rgba(108,99,255,0.45);letter-spacing:0.3px;transition:transform 0.15s,box-shadow 0.15s;" onmouseover="this.style.transform='translateY(-2px)';this.style.boxShadow='0 10px 30px rgba(108,99,255,0.55)'" onmouseout="this.style.transform='';this.style.boxShadow='0 6px 24px rgba(108,99,255,0.45)'">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
|
</svg>
|
|
Generar Intención de Pago
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="stripeHistoryContainer" style="display:none; margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
<h4 class="section-title" style="margin-bottom: 0;">Historial de Intenciones de Pago por transferencia</h4>
|
|
<div id="stripeCashBalanceBadge" class="balance-badge" style="display:none;">
|
|
<img src="?action=image&file=account-balance.webp" style="width: 32px; height: 32px; margin-right: 8px;">
|
|
<span id="stripeCashBalanceText">Saldo Stripe: $0.00 MXN</span>
|
|
</div>
|
|
</div>
|
|
<div style="overflow-x: auto;">
|
|
<table class="table" id="stripeHistoryTable" style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr style="text-align: left; background: var(--bg-body);">
|
|
<th style="padding: 10px;">Estado</th>
|
|
<th style="padding: 10px;">Importe</th>
|
|
<th style="padding: 10px;">Descripción</th>
|
|
<th style="padding: 10px;">Fecha</th>
|
|
<th style="padding: 10px;">ID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php else: ?>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// 2. STRIPE SEARCH
|
|
let selectedStripeClient = null;
|
|
setupSearch('stripeSearch', 'stripeResults', 'search_stripe', async (partialClient) => {
|
|
const res = await fetch(`${window.SIIP_STRIPE_PATH || ''}?action=get_stripe_details&id=${partialClient.id}`);
|
|
const data = await res.json();
|
|
selectedStripeClient = data;
|
|
|
|
document.getElementById('stripeClientName').textContent = data.fullName;
|
|
document.getElementById('stripeClientIdDisplay').textContent = `ID: #${data.id}`;
|
|
|
|
// Saldo en CRM: usar accountBalance (crédito a favor) o accountOutstanding (adeudo)
|
|
const crmCredit = parseFloat(data.accountBalance || 0);
|
|
const crmOwed = parseFloat(data.accountOutstanding || 0);
|
|
const balanceBadge = document.getElementById('stripeBalanceBadge');
|
|
if (crmCredit > 0) {
|
|
balanceBadge.textContent = `$${crmCredit.toFixed(2)} MXN`;
|
|
balanceBadge.style.color = 'var(--success)';
|
|
} else if (crmOwed > 0) {
|
|
balanceBadge.textContent = `-$${crmOwed.toFixed(2)} MXN`;
|
|
balanceBadge.style.color = 'var(--danger)';
|
|
} else {
|
|
balanceBadge.textContent = '$0.00 MXN';
|
|
balanceBadge.style.color = 'inherit';
|
|
}
|
|
|
|
document.getElementById('stripeCustomerIdDisplay').textContent = data.stripeCustomerId || 'No disponible';
|
|
document.getElementById('stripeClabeDisplay').textContent = data.clabeInterbancaria || 'No disponible';
|
|
document.getElementById('stripeAmount').value = crmOwed > 0 ? crmOwed : '';
|
|
document.getElementById('btnVerEnCrm').href = `${store.publicUrl}/client/${data.id}`;
|
|
|
|
// Mostrar domicilio del cliente
|
|
const addrEl = document.getElementById('stripeClientAddressDisplay');
|
|
if (data.fullAddress) {
|
|
addrEl.textContent = '📍 ' + data.fullAddress;
|
|
addrEl.style.display = 'block';
|
|
} else {
|
|
addrEl.style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('stripeDetailContainer').style.display = 'block';
|
|
document.getElementById('stripePlaceholder').style.display = 'none';
|
|
if (data.stripeCustomerId) loadStripeHistory(data.stripeCustomerId);
|
|
else document.getElementById('stripeHistoryContainer').style.display = 'none';
|
|
});
|
|
|
|
document.getElementById('btnCreateIntent').onclick = async () => {
|
|
if (!selectedStripeClient?.stripeCustomerId) return showToast('Error: Cliente sin Stripe ID', true);
|
|
const amt = parseFloat(document.getElementById('stripeAmount').value);
|
|
if (!amt || amt < 10) return showToast('Mínimo 10 MXN', true);
|
|
|
|
const btn=document.getElementById('btnCreateIntent');
|
|
btn.disabled=true;
|
|
btn.textContent='Procesando...' ;
|
|
|
|
const fd=new FormData();
|
|
fd.append('action', 'create_intent' );
|
|
fd.append('clientId', selectedStripeClient.id);
|
|
fd.append('amount', amt);
|
|
fd.append('stripeCustomerId', selectedStripeClient.stripeCustomerId);
|
|
fd.append('adminId', document.getElementById('stripeAdminSelect').value || store.defaultStripeAdminId);
|
|
|
|
try {
|
|
const res=await fetch(`${window.SIIP_STRIPE_PATH || '' }?`, {
|
|
method: 'POST' ,
|
|
body: fd
|
|
});
|
|
const d=await res.json();
|
|
if (d.success) showStripeResult(d);
|
|
else showToast(d.error, true);
|
|
} catch (e) {
|
|
showToast('Error de conexión', true);
|
|
}
|
|
btn.disabled=false;
|
|
btn.innerHTML=`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg> Generar Intención de Pago`;
|
|
};
|
|
|
|
async function loadStripeHistory(stripeCustomerId) {
|
|
const container = document.getElementById('stripeHistoryContainer');
|
|
const tbody = document.querySelector('#stripeHistoryTable tbody');
|
|
const cashBadge = document.getElementById('stripeCashBalanceBadge');
|
|
const cashText = document.getElementById('stripeCashBalanceText');
|
|
|
|
try {
|
|
container.style.display = 'block';
|
|
tbody.innerHTML = `<tr>
|
|
<td colspan="5" style="text-align:center;padding:2rem;">Cargando historial...</td>
|
|
</tr>`;
|
|
|
|
const res = await fetch(`${window.SIIP_STRIPE_PATH || ''}?action=get_stripe_history&customerId=${stripeCustomerId}`);
|
|
const d = await res.json();
|
|
|
|
if (d.cashBalance !== undefined) {
|
|
const balanceMxn = (d.cashBalance / 100).toFixed(2);
|
|
// Solo actualizar badge del historial (saldo en caja de Stripe)
|
|
// El badge principal (#stripeBalanceBadge) muestra el saldo del CRM, no lo tocamos
|
|
cashBadge.style.display = 'flex';
|
|
cashText.textContent = `Saldo en caja Stripe: $${balanceMxn} MXN`;
|
|
}
|
|
|
|
if (!d.payments || d.payments.length === 0) {
|
|
tbody.innerHTML = `<tr>
|
|
<td colspan="5" style="text-align:center;padding:2rem;color:var(--text-muted)">No hay intenciones de pago recientes</td>
|
|
</tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = d.payments.map(p => {
|
|
let statusColor = 'var(--text-muted)';
|
|
let statusLabel = p.status.toUpperCase();
|
|
if (p.status === 'succeeded') { statusColor = 'var(--success)'; statusLabel = 'PAGADO'; }
|
|
if (p.status === 'processing') { statusColor = 'var(--warning)'; statusLabel = 'PENDIENTE'; }
|
|
|
|
// Badge construido con concatenación para evitar falsos errores del CSS linter
|
|
const badgeHtml = '<span class="badge" style="background:' + statusColor + '20; color:' + statusColor + '; border:1px solid ' + statusColor + '40;">' + statusLabel + '</span>';
|
|
|
|
return `
|
|
<tr style="border-bottom: 1px solid var(--border);">
|
|
<td style="padding:12px;">${badgeHtml}</td>
|
|
<td style="padding:12px; font-weight:600;">$${p.amount.toFixed(2)}</td>
|
|
<td style="padding:12px; font-size:0.9rem;">${p.description}</td>
|
|
<td style="padding:12px; font-size:0.85rem; color:var(--text-muted);">${p.date}</td>
|
|
<td style="padding:12px; font-family:monospace; font-size:0.8rem; color:var(--text-muted);">${p.id}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
} catch (e) {
|
|
console.error(e);
|
|
tbody.innerHTML = `<tr>
|
|
<td colspan="5" style="text-align:center;color:var(--danger)">Error de conexión</td>
|
|
</tr>`;
|
|
}
|
|
}
|
|
|
|
function showStripeResult(data) {
|
|
const c = document.getElementById('stripeResultContent');
|
|
let html = `<div style="text-align:center; margin-bottom: 20px;">
|
|
<h3 style="color:var(--success); margin-bottom: 5px;">¡Referencia Creada!</h3>
|
|
<p style="font-size: 1.1rem; color: var(--text-main);">Monto: <strong>$${data.amount} MXN</strong></p>
|
|
</div>`;
|
|
|
|
if (data.next_action?.display_bank_transfer_instructions) {
|
|
const instr = data.next_action.display_bank_transfer_instructions;
|
|
const spei = instr.financial_addresses[0]?.spei;
|
|
|
|
if (spei) {
|
|
html += `
|
|
<div style="background:#f8fafc; border: 1px solid #e2e8f0; padding:1.5rem; border-radius:12px; color: #1e293b; text-align: left;">
|
|
<div style="margin-bottom: 1rem;">
|
|
<span style="display:block; font-size: 0.8rem; color: #64748b; text-transform: uppercase; font-weight: 600;">Institución Bancaria</span>
|
|
<strong style="font-size: 1.1rem;">${spei.bank_name}</strong>
|
|
</div>
|
|
<div style="margin-bottom: 0;">
|
|
<span style="display:block; font-size: 0.8rem; color: #64748b; text-transform: uppercase; font-weight: 600;">CLABE Interbancaria</span>
|
|
<strong style="font-size: 1.3rem; letter-spacing: 1px; font-family: monospace; display: block; margin-top: 5px;">${spei.clabe}</strong>
|
|
</div>
|
|
</div>
|
|
<p style="font-size: 0.85rem; color: var(--text-muted); text-align: center; margin-top: 1.5rem;">
|
|
Realiza tu transferencia vía SPEI con estos datos. El pago se registrará automáticamente al recibirse.
|
|
</p>`;
|
|
} else {
|
|
html += `<div style="background:rgba(255,165,0,0.1); padding: 15px; border-radius: 8px; border: 1px solid orange; color: orange; text-align:center;">
|
|
Se generó la intención pero no se encontraron datos de transferencia SPEI.<br>Por favor, revisa el historial para ver si ya existe una CLABE asignada.
|
|
</div>`;
|
|
}
|
|
} else if (data.status === 'succeeded') {
|
|
html += `<div style="background:rgba(34,197,94,0.1); padding:18px; border-radius:10px; border:1px solid var(--success); color:var(--success); text-align:center;">
|
|
<div style="font-size:2rem; margin-bottom:8px;">✅</div>
|
|
<strong style="font-size:1.05rem;">Pago procesado automáticamente</strong>
|
|
<p style="margin:8px 0 0 0; font-size:0.9rem; opacity:0.85;">El saldo disponible en la caja de Stripe cubrió el monto completo.<br>No se requiere transferencia bancaria.</p>
|
|
</div>`;
|
|
} else if (data.status === 'processing') {
|
|
html += `<div style="background:rgba(59,130,246,0.1); padding:18px; border-radius:10px; border:1px solid #3b82f6; color:#3b82f6; text-align:center;">
|
|
<div style="font-size:2rem; margin-bottom:8px;">⏳</div>
|
|
<strong style="font-size:1.05rem;">Pago en proceso</strong>
|
|
<p style="margin:8px 0 0 0; font-size:0.9rem; opacity:0.85;">La transacción se está procesando. Revisa el historial en unos momentos.</p>
|
|
</div>`;
|
|
} else {
|
|
html += `<div style="background:rgba(255,165,0,0.1); padding: 15px; border-radius: 8px; border: 1px solid orange; color: orange; text-align:center;">
|
|
Estado: ${data.status || 'Desconocido'}.<br>No se encontró información de transferencia. Por favor, revisa el historial.
|
|
</div>`;
|
|
}
|
|
|
|
c.innerHTML = html;
|
|
document.getElementById('stripeResultModal').style.display = 'flex';
|
|
}
|
|
});
|
|
<?php endif; ?>
|