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;"> Generador de Intenciones de Pago Stripe
|
|
</h2>
|
|
<p style="color: var(--text-muted); margin: 5px 0 0 0;">🏦Genera intenciones de pago en línea personalizadas para liberar saldo no reflejado en la cuenta del cliente en UISP CRM.</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 consultar sus movimientos en Stripe y generar una referencia de pago.</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; ?>
|