22// Pacifictrans Go - Interactive Web Platform Logic
let currentTariffs = {
tarifaPasajeroFija: 35000,
precioBasePaquete: 25000,
precioKmPaquete: 1200,
factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 },
seguroBasePaquete: 5000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
radioMaxPoolingMetros: 4000,
numeroAtencionWhatsApp: '573167455877',
nombreSoporte: 'Atención Pacifictrans Go'
};
let currentUser = null;
let leafletMap = null;
let deferredPrompt = null;
// Drivers Flota con coordenadas reales en el corredor Cali - Buenaventura
const activeDrivers = [
{ id: '1', nombre: 'Mario Fernando G.', vehiculo: 'Renault Duster (WLZ-890)', barrio: 'Granada (Comuna 2)', distCali: '1.2 km', lat: 3.4552, lng: -76.5350, estado: 'DISPONIBLE' },
{ id: '2', nombre: 'Hernán Darío P.', vehiculo: 'Toyota Fortuner (VBF-312)', barrio: 'San Antonio (Comuna 3)', distCali: '2.4 km', lat: 3.4485, lng: -76.5415, estado: 'ASIGNADO' },
{ id: '3', nombre: 'Mauricio V.', vehiculo: 'Chevrolet Tracker (HJK-456)', barrio: 'El Ingenio (Comuna 17)', distCali: '6.8 km', lat: 3.3850, lng: -76.5290, estado: 'DISPONIBLE' },
{ id: '4', nombre: 'Javier Restrepo', vehiculo: 'Nissan Kicks (RTY-789)', barrio: 'Limonar (Comuna 19)', distCali: '5.1 km', lat: 3.6580, lng: -76.6900, estado: 'EN_RUTA' }
];
document.addEventListener('DOMContentLoaded', () => {
initPWA();
initTabs();
initAuthSystem();
initNotificationsSystem();
initPassengerCalculator();
initFreightCalculator();
initLeafletRadarMap();
initReviewsAndStats();
initTariffsManager();
initCouponsManager();
initSettlementsAndBreBSystem();
fetchTariffs();
fetchAdminCoupons();
fetchStatsAndReviews();
});
/* ==========================================================================
1. PWA & IPHONE INSTALLATION SYSTEM
========================================================================== */
function initPWA() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('✅ [PWA] Service Worker registrado exitosamente:', reg.scope))
.catch(err => console.warn('⚠️ [PWA Service Worker Warning]:', err));
});
}
// Guardar evento de instalación de Android
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
});
const btnInstall = document.getElementById('btn-install-pwa');
const modalInstall = document.getElementById('modal-pwa-install');
const btnClose = document.getElementById('btn-close-pwa-modal');
const btnTriggerAndroid = document.getElementById('btn-trigger-android-pwa');
if (btnInstall && modalInstall) {
btnInstall.addEventListener('click', () => {
modalInstall.classList.add('active');
});
}
if (btnClose && modalInstall) {
btnClose.addEventListener('click', () => {
modalInstall.classList.remove('active');
});
modalInstall.addEventListener('click', (e) => {
if (e.target === modalInstall) modalInstall.classList.remove('active');
});
}
if (btnTriggerAndroid) {
btnTriggerAndroid.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('PWA Outcome:', outcome);
deferredPrompt = null;
if (modalInstall) modalInstall.classList.remove('active');
} else {
alert('Para instalar en iPhone, sigue los 4 pasos mostrados arriba en Safari. En Android Chrome, toca los 3 puntos (⋮) y selecciona "Instalar aplicación".');
}
});
}
}
/* ==========================================================================
2. TABS & ADMIN SUBTABS NAVIGATION
========================================================================== */
function initTabs() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
// Switcher de Vistas Principales SPA con Escudo de Seguridad
window.showView = function (viewName) {
const viewPublic = document.getElementById('view-public-portal');
const viewAdmin = document.getElementById('view-admin-dashboard');
const viewDriver = document.getElementById('view-driver-portal');
const adminGate = document.getElementById('admin-security-gate');
const adminContent = document.getElementById('admin-console-content');
const driverGate = document.getElementById('driver-security-gate');
const driverContent = document.getElementById('driver-console-content');
// Ocultar todas las vistas principales
if (viewPublic) viewPublic.style.display = 'none';
if (viewAdmin) viewAdmin.style.display = 'none';
if (viewDriver) viewDriver.style.display = 'none';
// Deseleccionar enlaces superiores
document.querySelectorAll('.nav-links a').forEach(a => a.classList.remove('active'));
if (viewName === 'admin') {
if (viewAdmin) viewAdmin.style.display = 'block';
const isAdmin = currentUser && (currentUser.rol === 'ADMIN' || currentUser.rol === 'OPERADOR');
if (isAdmin) {
if (adminGate) adminGate.style.display = 'none';
if (adminContent) adminContent.style.display = 'block';
const adminNameEl = document.getElementById('admin-user-name');
if (adminNameEl) adminNameEl.textContent = `${currentUser.nombre} (${currentUser.rol})`;
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 250);
if (window.fetchSettlementsOverview) window.fetchSettlementsOverview();
if (window.fetchAdminPayoutRequests) window.fetchAdminPayoutRequests();
} else {
if (adminGate) adminGate.style.display = 'block';
if (adminContent) adminContent.style.display = 'none';
}
} else if (viewName === 'driver') {
if (viewDriver) viewDriver.style.display = 'block';
const isDriver = currentUser && (currentUser.rol === 'CONDUCTOR' || currentUser.rol === 'ADMIN');
if (isDriver) {
if (driverGate) driverGate.style.display = 'none';
if (driverContent) driverContent.style.display = 'block';
const driverNameEl = document.getElementById('driver-active-name');
if (driverNameEl) driverNameEl.textContent = currentUser.nombre;
if (window.fetchDriverWallet) window.fetchDriverWallet();
if (window.fetchDriverPayoutRequests) window.fetchDriverPayoutRequests();
} else {
if (driverGate) driverGate.style.display = 'block';
if (driverContent) driverContent.style.display = 'none';
}
} else {
// Default: Portal Público limpio
if (viewPublic) viewPublic.style.display = 'block';
const linkReservas = document.getElementById('nav-link-reservas');
if (linkReservas) linkReservas.classList.add('active');
}
};
// Selector elegante de servicios públicos: Pasajeros vs Encomiendas
window.activatePublicService = function (targetTabId) {
const btnPasajeros = document.getElementById('btn-service-pasajeros');
const btnFletes = document.getElementById('btn-service-fletes');
const tabPasajeros = document.getElementById('tab-pasajeros');
const tabFletes = document.getElementById('tab-fletes');
if (targetTabId === 'tab-fletes') {
if (btnPasajeros) btnPasajeros.classList.remove('active');
if (btnFletes) btnFletes.classList.add('active');
if (tabPasajeros) tabPasajeros.classList.remove('active');
if (tabFletes) tabFletes.classList.add('active');
} else {
if (btnPasajeros) btnPasajeros.classList.add('active');
if (btnFletes) btnFletes.classList.remove('active');
if (tabPasajeros) tabPasajeros.classList.add('active');
if (tabFletes) tabFletes.classList.remove('active');
}
};
const btnServicePasajeros = document.getElementById('btn-service-pasajeros');
const btnServiceFletes = document.getElementById('btn-service-fletes');
if (btnServicePasajeros) {
btnServicePasajeros.addEventListener('click', () => window.activatePublicService('tab-pasajeros'));
}
if (btnServiceFletes) {
btnServiceFletes.addEventListener('click', () => window.activatePublicService('tab-fletes'));
}
// Enlaces públicos del navbar y hero
const navLinkReservas = document.getElementById('nav-link-reservas');
if (navLinkReservas) {
navLinkReservas.addEventListener('click', (e) => {
e.preventDefault();
window.showView('public');
window.activatePublicService('tab-pasajeros');
document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' });
});
}
const navLinkFletes = document.getElementById('nav-link-fletes');
const heroBtnFlete = document.getElementById('hero-btn-flete');
[navLinkFletes, heroBtnFlete].forEach(el => {
if (el) {
el.addEventListener('click', (e) => {
e.preventDefault();
window.showView('public');
window.activatePublicService('tab-fletes');
document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' });
});
}
});
const navLinkReviews = document.getElementById('nav-link-reviews');
if (navLinkReviews) {
navLinkReviews.addEventListener('click', (e) => {
e.preventDefault();
window.showView('public');
document.getElementById('estadisticas')?.scrollIntoView({ behavior: 'smooth' });
});
}
// Botones de acceso exclusivo para Roles en Navbar
const navBtnOpenAdmin = document.getElementById('nav-btn-open-admin');
if (navBtnOpenAdmin) {
navBtnOpenAdmin.addEventListener('click', (e) => {
e.preventDefault();
window.location.hash = '#admin';
window.showView('admin');
});
}
const navBtnOpenDriver = document.getElementById('nav-btn-open-driver');
if (navBtnOpenDriver) {
navBtnOpenDriver.addEventListener('click', (e) => {
e.preventDefault();
window.location.hash = '#conductor';
window.showView('driver');
});
}
// Botones de regreso al sitio público
const btnAdminViewPublic = document.getElementById('btn-admin-view-public');
const btnDriverViewPublic = document.getElementById('btn-driver-view-public');
const btnGateBackPublic = document.getElementById('btn-gate-back-public');
const btnGateDriverBack = document.getElementById('btn-gate-driver-back-public');
[btnAdminViewPublic, btnDriverViewPublic, btnGateBackPublic, btnGateDriverBack].forEach(b => {
if (b) {
b.addEventListener('click', () => {
window.location.hash = '#reservas';
window.showView('public');
});
}
});
// Botones para abrir modal de login desde los escudos de seguridad
const btnGateAdminLogin = document.getElementById('btn-gate-admin-login');
const btnGateDriverLogin = document.getElementById('btn-gate-driver-login');
const authModal = document.getElementById('modal-auth-dripzone');
[btnGateAdminLogin, btnGateDriverLogin].forEach(b => {
if (b && authModal) {
b.addEventListener('click', () => authModal.classList.add('active'));
}
});
const btnAdminLogout = document.getElementById('btn-admin-logout');
if (btnAdminLogout) {
btnAdminLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
currentUser = null;
if (window.updateUserUI) window.updateUserUI(null);
window.location.hash = '#reservas';
window.showView('public');
});
}
// Compatibilidad genérica para pestañas
window.activateTab = function (targetId) {
tabBtns.forEach(b => {
if (b.getAttribute('data-tab') === targetId) {
b.classList.add('active');
} else {
b.classList.remove('active');
}
});
tabContents.forEach(c => {
if (c.id === targetId) {
c.classList.add('active');
} else {
c.classList.remove('active');
}
});
if (targetId === 'tab-admin' && leafletMap) {
setTimeout(() => {
leafletMap.invalidateSize();
}, 250);
}
};
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetId = btn.getAttribute('data-tab');
window.activateTab(targetId);
});
});
// Sub-tabs de la sección administrativa
const viewRadar = document.getElementById('admin-view-radar');
const viewTarifas = document.getElementById('admin-view-tarifas');
const viewLiquidaciones = document.getElementById('admin-view-liquidaciones');
const viewCupones = document.getElementById('admin-view-cupones');
function setAdminSubTab(targetViewId) {
const allViews = [
{ id: 'radar', el: viewRadar },
{ id: 'tarifas', el: viewTarifas },
{ id: 'liquidaciones', el: viewLiquidaciones },
{ id: 'cupones', el: viewCupones }
];
allViews.forEach(v => {
if (v.el) v.style.display = (v.id === targetViewId) ? 'block' : 'none';
});
document.querySelectorAll('.admin-pill-btn').forEach(btn => {
const id = btn.id || '';
if (id.includes(targetViewId)) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
if (targetViewId === 'radar' && leafletMap) {
setTimeout(() => leafletMap.invalidateSize(), 200);
}
if (targetViewId === 'liquidaciones' && window.fetchSettlementsOverview) {
window.fetchSettlementsOverview();
}
if (targetViewId === 'cupones' && window.fetchAdminCoupons) {
window.fetchAdminCoupons();
}
}
document.querySelectorAll('#btn-admin-subtab-radar').forEach(b => b.addEventListener('click', () => setAdminSubTab('radar')));
document.querySelectorAll('#btn-admin-subtab-tarifas').forEach(b => b.addEventListener('click', () => setAdminSubTab('tarifas')));
document.querySelectorAll('#btn-admin-subtab-liquidaciones').forEach(b => b.addEventListener('click', () => setAdminSubTab('liquidaciones')));
document.querySelectorAll('#btn-admin-subtab-cupones').forEach(b => b.addEventListener('click', () => setAdminSubTab('cupones')));
// Sub-tabs de la sección del conductor
const subDriverWalletBtn = document.getElementById('btn-driver-subtab-billetera');
const subDriverRegistroBtn = document.getElementById('btn-driver-subtab-registro');
const viewDriverWallet = document.getElementById('driver-view-billetera');
const viewDriverRegistro = document.getElementById('driver-view-registro');
if (subDriverWalletBtn && subDriverRegistroBtn && viewDriverWallet && viewDriverRegistro) {
subDriverWalletBtn.addEventListener('click', () => {
subDriverWalletBtn.classList.add('active');
subDriverRegistroBtn.classList.remove('active');
viewDriverWallet.style.display = 'block';
viewDriverRegistro.style.display = 'none';
if (window.fetchDriverWallet) window.fetchDriverWallet();
});
subDriverRegistroBtn.addEventListener('click', () => {
subDriverRegistroBtn.classList.add('active');
subDriverWalletBtn.classList.remove('active');
viewDriverWallet.style.display = 'none';
viewDriverRegistro.style.display = 'block';
});
}
// Enrutamiento por Hash en la URL (#admin, #conductor, etc.)
function handleUrlHash() {
const hash = (window.location.hash || '').toLowerCase();
if (hash === '#admin') {
window.showView('admin');
} else if (hash === '#conductor' || hash === '#driver') {
window.showView('driver');
} else {
window.showView('public');
if (hash === '#fletes') {
window.activatePublicService('tab-fletes');
}
}
}
window.addEventListener('hashchange', handleUrlHash);
handleUrlHash();
}
/* ==========================================================================
3. AUTHENTICATION (DRIPZONE STYLE CON OTP WHATSAPP + EMAIL)
========================================================================== */
function initAuthSystem() {
const modal = document.getElementById('modal-auth-dripzone');
const btnOpen = document.getElementById('btn-open-auth-modal');
const btnClose = document.getElementById('btn-close-auth-modal');
const btnLogout = document.getElementById('btn-logout');
const modeLoginBtn = document.getElementById('btn-auth-mode-login');
const modeRegisterBtn = document.getElementById('btn-auth-mode-register');
const groupName = document.getElementById('group-auth-name');
const authModalTitle = document.getElementById('auth-modal-title');
const step1 = document.getElementById('auth-step-1');
const step2 = document.getElementById('auth-step-2');
const formStep1 = document.getElementById('form-auth-request-otp');
const btnVerifyOtp = document.getElementById('btn-submit-verify-otp');
const btnResend = document.getElementById('btn-auth-resend-otp');
const reqError = document.getElementById('auth-request-error');
const verError = document.getElementById('auth-verify-error');
const targetLabel = document.getElementById('auth-sent-target');
let authMode = 'login'; // 'login' | 'register'
let currentIdentifier = '';
let currentEmail = '';
// Abrir y cerrar modal
if (btnOpen && modal) {
btnOpen.addEventListener('click', () => {
reqError.style.display = 'none';
verError.style.display = 'none';
step1.style.display = 'block';
step2.style.display = 'none';
modal.classList.add('active');
});
}
if (btnClose && modal) {
btnClose.addEventListener('click', () => modal.classList.remove('active'));
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.classList.remove('active');
});
}
// Alternar entre Login y Registro
if (modeLoginBtn && modeRegisterBtn) {
modeLoginBtn.addEventListener('click', () => {
authMode = 'login';
modeLoginBtn.classList.add('active');
modeRegisterBtn.classList.remove('active');
if (groupName) groupName.style.display = 'none';
if (authModalTitle) authModalTitle.textContent = 'Acceso Seguro con Verificación OTP';
});
modeRegisterBtn.addEventListener('click', () => {
authMode = 'register';
modeRegisterBtn.classList.add('active');
modeLoginBtn.classList.remove('active');
if (groupName) groupName.style.display = 'block';
if (authModalTitle) authModalTitle.textContent = 'Registro de Usuario & Validación 2FA';
});
}
// Paso 1: Enviar OTP por WhatsApp
if (formStep1) {
formStep1.addEventListener('submit', async (e) => {
e.preventDefault();
const phoneInput = document.getElementById('auth-input-phone');
const emailInput = document.getElementById('auth-input-email');
const nameInput = document.getElementById('auth-input-name');
const btnSubmit = document.getElementById('btn-submit-send-otp');
const phone = phoneInput?.value.trim() || '';
const email = emailInput?.value.trim() || '';
const name = nameInput?.value.trim() || '';
if (!phone) {
showError(reqError, 'Ingresa tu número de WhatsApp.');
return;
}
currentIdentifier = phone;
currentEmail = email;
btnSubmit.disabled = true;
btnSubmit.innerHTML = '⏳ Enviando código de seguridad...';
reqError.style.display = 'none';
try {
const res = await fetch('/api/auth/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: phone, email })
});
const data = await res.json();
if (res.ok && data.success) {
step1.style.display = 'none';
step2.style.display = 'block';
if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${phone}`;
// Si el servidor devolvió preview (entorno local sin SIM de Evolution), mostrar aviso
if (data.codePreview) {
console.log(`[Demo Code OTP]: ${data.codePreview}`);
verError.style.display = 'block';
verError.style.color = '#38bdf8';
verError.innerHTML = `ℹ️ Código de prueba generado: ${data.codePreview} `;
}
focusFirstOtp();
} else {
showError(reqError, data.error || 'No se pudo enviar el código OTP.');
}
} catch (err) {
showError(reqError, 'Error de conexión. Inténtalo de nuevo.');
} finally {
btnSubmit.disabled = false;
btnSubmit.innerHTML = '📲 Enviar Código de Verificación OTP';
}
});
}
// Cajas OTP con avance automático y soporte de pegado
const otpInputs = [
document.getElementById('otp-1'),
document.getElementById('otp-2'),
document.getElementById('otp-3'),
document.getElementById('otp-4'),
document.getElementById('otp-5'),
document.getElementById('otp-6')
];
otpInputs.forEach((input, index) => {
if (!input) return;
input.addEventListener('input', (e) => {
const val = e.target.value;
if (val.length === 1 && index < 5) {
otpInputs[index + 1].focus();
}
checkAutoSubmitOtp();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !input.value && index > 0) {
otpInputs[index - 1].focus();
}
});
input.addEventListener('paste', (e) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text').trim();
if (/^\d{6}$/.test(pasteData)) {
pasteData.split('').forEach((digit, i) => {
if (otpInputs[i]) otpInputs[i].value = digit;
});
checkAutoSubmitOtp();
}
});
});
function focusFirstOtp() {
otpInputs.forEach(i => { if (i) i.value = ''; });
if (otpInputs[0]) otpInputs[0].focus();
}
function getEnteredOtp() {
return otpInputs.map(i => i?.value || '').join('');
}
function checkAutoSubmitOtp() {
const code = getEnteredOtp();
if (code.length === 6) {
submitVerifyOtp(code);
}
}
// Paso 2: Verificar OTP
if (btnVerifyOtp) {
btnVerifyOtp.addEventListener('click', () => {
const code = getEnteredOtp();
if (code.length < 6) {
showError(verError, 'Ingresa los 6 dígitos del código.');
return;
}
submitVerifyOtp(code);
});
}
async function submitVerifyOtp(code) {
if (!btnVerifyOtp) return;
btnVerifyOtp.disabled = true;
btnVerifyOtp.innerHTML = '⏳ Verificando código...';
verError.style.display = 'none';
const nameInput = document.getElementById('auth-input-name');
const name = nameInput?.value.trim() || '';
try {
const res = await fetch('/api/auth/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identifier: currentIdentifier,
code,
nombre: name,
email: currentEmail
})
});
const data = await res.json();
if (res.ok && data.success) {
localStorage.setItem('pt_token', data.token);
currentUser = data.user;
updateUserUI(currentUser);
modal.classList.remove('active');
alert(`¡Bienvenido a Pacifictrans Go, ${currentUser.nombre}! Tu identidad ha sido verificada con éxito.`);
} else {
showError(verError, data.error || 'Código incorrecto. Verifica los 6 dígitos.');
}
} catch (err) {
showError(verError, 'Error al validar el código OTP.');
} finally {
btnVerifyOtp.disabled = false;
btnVerifyOtp.innerHTML = '✅ Validar y Acceder';
}
}
// Reenviar OTP
if (btnResend) {
btnResend.addEventListener('click', () => {
formStep1.dispatchEvent(new Event('submit'));
});
}
// Selector Rápido de Roles para Auditoría y Pruebas
document.querySelectorAll('.btn-role-quick-login').forEach(btn => {
btn.addEventListener('click', async () => {
const role = btn.getAttribute('data-role') || 'ADMIN';
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '⏳ Accediendo...';
try {
const res = await fetch('/api/auth/quick-role-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role })
});
const data = await res.json();
if (data.success && data.user) {
localStorage.setItem('pt_token', data.token);
currentUser = data.user;
updateUserUI(currentUser);
if (modal) modal.classList.remove('active');
if (role === 'ADMIN') {
window.location.hash = '#admin';
window.showView('admin');
} else if (role === 'CONDUCTOR') {
window.location.hash = '#conductor';
window.showView('driver');
} else {
window.location.hash = '#reservas';
window.showView('public');
}
} else {
alert('Error de acceso rápido: ' + (data.error || 'Intenta de nuevo'));
}
} catch (e) {
console.error('Quick role error:', e);
alert('Error de conexión al servidor');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
});
// Cerrar sesión
if (btnLogout) {
btnLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
currentUser = null;
updateUserUI(null);
window.location.hash = '#reservas';
window.showView('public');
});
}
// Validar sesión persistida
checkPersistedSession();
}
async function checkPersistedSession() {
const token = localStorage.getItem('pt_token');
if (!token) return;
try {
const res = await fetch(`/api/auth/me?token=${token}`);
if (res.ok) {
const data = await res.json();
if (data.user) {
currentUser = data.user;
updateUserUI(currentUser);
}
} else {
localStorage.removeItem('pt_token');
}
} catch (err) {
console.warn('⚠️ [Session Verify Error]:', err);
}
}
function updateUserUI(user) {
const btnAuthOpen = document.getElementById('btn-open-auth-modal');
const userBadge = document.getElementById('user-profile-badge');
const userNameEl = document.getElementById('user-display-name');
const userRoleBadge = document.getElementById('user-role-badge');
const navBtnAdmin = document.getElementById('nav-btn-open-admin');
const navBtnDriver = document.getElementById('nav-btn-open-driver');
const inputUserName = document.getElementById('user-name');
const inputUserPhone = document.getElementById('user-phone');
const inputSenderPhone = document.getElementById('sender-phone');
if (user) {
if (btnAuthOpen) btnAuthOpen.style.display = 'none';
if (userBadge) userBadge.style.display = 'inline-flex';
if (userNameEl) userNameEl.textContent = user.nombre.split(' ')[0];
if (userRoleBadge) userRoleBadge.textContent = `✅ ${user.rol}`;
// Mostrar accesos a consolas según rol del usuario
if (navBtnAdmin) {
navBtnAdmin.style.display = (user.rol === 'ADMIN' || user.rol === 'OPERADOR') ? 'inline-flex' : 'none';
}
if (navBtnDriver) {
navBtnDriver.style.display = (user.rol === 'CONDUCTOR' || user.rol === 'ADMIN') ? 'inline-flex' : 'none';
}
// Autofill en formularios de la plataforma
if (inputUserName && !inputUserName.value) inputUserName.value = user.nombre;
if (inputUserPhone && !inputUserPhone.value) inputUserPhone.value = user.telefono;
if (inputSenderPhone && !inputSenderPhone.value) inputSenderPhone.value = user.telefono;
const p1Name = document.getElementById('passenger-1-name');
const p1Phone = document.getElementById('passenger-1-phone');
if (p1Name && !p1Name.value) p1Name.value = user.nombre;
if (p1Phone && !p1Phone.value) p1Phone.value = user.telefono;
// Actualizar vista actual si está en zona protegida
const hash = (window.location.hash || '').toLowerCase();
if (hash === '#admin') window.showView('admin');
if (hash === '#conductor' || hash === '#driver') window.showView('driver');
} else {
if (btnAuthOpen) btnAuthOpen.style.display = 'inline-flex';
if (userBadge) userBadge.style.display = 'none';
if (navBtnAdmin) navBtnAdmin.style.display = 'none';
if (navBtnDriver) navBtnDriver.style.display = 'none';
const hash = (window.location.hash || '').toLowerCase();
if (hash === '#admin' || hash === '#conductor' || hash === '#driver') {
window.showView(hash === '#admin' ? 'admin' : 'driver');
}
}
}
function showError(el, msg) {
if (!el) return;
el.textContent = msg;
el.style.display = 'block';
}
/* ==========================================================================
4. LEAFLET RADAR FLEET MAP (MAPA ESTÁNDAR CON TOOLTIPS)
========================================================================== */
function initLeafletRadarMap() {
const mapContainer = document.getElementById('fleet-live-map');
if (!mapContainer || typeof L === 'undefined') return;
// Centro entre Cali (3.45) y Buenaventura (3.88)
const corridorCenter = [3.65, -76.78];
leafletMap = L.map('fleet-live-map', {
center: corridorCenter,
zoom: 10,
zoomControl: true,
scrollWheelZoom: false
});
// Capa de Mapa Estándar OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap contributors • Pacifictrans Go GPS'
}).addTo(leafletMap);
// Línea poligonal oficial de la Carretera Vía al Mar (Ruta 40)
const corridorWaypoints = [
[3.4516, -76.5320], // Cali
[3.4750, -76.5650], // Portada al Mar
[3.5800, -76.6300], // Km 18 / Saladito
[3.6580, -76.6900], // Dagua
[3.7650, -76.7350], // Loboguerrero
[3.7920, -76.8150], // Cisneros
[3.8450, -76.9200], // Córdoba
[3.8814, -77.0315] // Buenaventura
];
L.polyline(corridorWaypoints, {
color: '#06b6d4',
weight: 5,
opacity: 0.85,
dashArray: '8, 6'
}).addTo(leafletMap);
// Marcadores de Hitos Urbanos
addCityMarker(leafletMap, [3.4516, -76.5320], '🟢 Cali (Km 0)');
addCityMarker(leafletMap, [3.6580, -76.6900], '🔵 Dagua (Km 45)');
addCityMarker(leafletMap, [3.7650, -76.7350], '🔵 Loboguerrero (Km 60)');
addCityMarker(leafletMap, [3.8814, -77.0315], '🟡 Buenaventura (Km 115)');
// Vehículos de la Flota en Tiempo Real
activeDrivers.forEach((driver, index) => {
const customIcon = L.divIcon({
className: 'vehicle-marker-icon',
html: `
🚗
`,
iconSize: [44, 44],
iconAnchor: [22, 22]
});
const marker = L.marker([driver.lat, driver.lng], { icon: customIcon }).addTo(leafletMap);
// Tooltip enriquecido al pasar el cursor o pulsar
const tooltipContent = `
`;
marker.bindTooltip(tooltipContent, {
direction: 'top',
className: 'custom-vehicle-tooltip',
offset: [0, -20]
});
marker.on('click', () => {
window.asignarConductorDemo(driver.nombre, driver.barrio);
});
});
renderSuggestedDriversList();
}
function addCityMarker(map, coords, label) {
const icon = L.divIcon({
className: 'city-marker-icon',
html: `${label}
`,
iconSize: [110, 26],
iconAnchor: [55, 13]
});
L.marker(coords, { icon }).addTo(map);
}
function renderSuggestedDriversList() {
const driverListEl = document.getElementById('suggested-drivers-list');
if (!driverListEl) return;
driverListEl.innerHTML = activeDrivers.map((d, index) => `
#${index + 1} ${d.nombre}
${d.vehiculo}
🏠 Residencia en Cali: ${d.barrio}
Distancia al 1er Pasajero: ${d.distCali}
${d.estado}
Asignar y Programar
`).join('');
}
window.asignarConductorDemo = function (conductor, barrio) {
const resultDiv = document.getElementById('schedule-dispatch-result');
if (resultDiv) {
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
✅ Conductor Asignado por Residencia: ${conductor} (${barrio})
🚀 Disparo Inmediato de Notificación WhatsApp Ejecutado:
Los 4 pasajeros han recibido su confirmación oficial con el conductor asignado, vehículo, hora estimada escalonada de recogida y enlace de Waze en vivo.
📲 [Evolution API WhatsApp Status: 201 Created] Enviado a los 4 Pasajeros + Conductor.
`;
resultDiv.scrollIntoView({ behavior: 'smooth' });
}
};
/* ==========================================================================
5. TRIP STATISTICS & COMMUNITY REVIEWS
========================================================================== */
function initReviewsAndStats() {
const btnOpenModal = document.getElementById('btn-open-review-modal');
const modalReview = document.getElementById('modal-add-review');
const btnCloseModal = document.getElementById('btn-close-review-modal');
const formReview = document.getElementById('form-submit-review');
const starPicker = document.getElementById('star-picker');
const hiddenScore = document.getElementById('review-input-score');
if (btnOpenModal && modalReview) {
btnOpenModal.addEventListener('click', () => {
if (currentUser && document.getElementById('review-input-name')) {
document.getElementById('review-input-name').value = currentUser.nombre;
}
modalReview.classList.add('active');
});
}
if (btnCloseModal && modalReview) {
btnCloseModal.addEventListener('click', () => modalReview.classList.remove('active'));
modalReview.addEventListener('click', (e) => {
if (e.target === modalReview) modalReview.classList.remove('active');
});
}
// Selector interactivo de estrellas (1-5)
if (starPicker) {
const stars = starPicker.querySelectorAll('span');
stars.forEach(star => {
star.addEventListener('click', () => {
const val = parseInt(star.getAttribute('data-star') || '5', 10);
if (hiddenScore) hiddenScore.value = val;
stars.forEach((s, idx) => {
if (idx < val) s.classList.add('selected');
else s.classList.remove('selected');
});
});
});
}
// Envío de reseña
if (formReview) {
formReview.addEventListener('submit', async (e) => {
e.preventDefault();
const btnSubmit = document.getElementById('btn-submit-review');
const score = parseFloat(hiddenScore?.value || '5');
const name = document.getElementById('review-input-name')?.value.trim();
const role = document.getElementById('review-input-role')?.value;
const target = document.getElementById('review-input-target')?.value.trim();
const comment = document.getElementById('review-input-comment')?.value.trim();
if (!name || !comment) return;
btnSubmit.disabled = true;
btnSubmit.innerHTML = '⏳ Publicando reseña...';
try {
const res = await fetch('/api/stats-reviews', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
autorId: currentUser?.id || `anon-${Date.now()}`,
autorNombre: name,
autorRol: role,
destinatarioNombre: target,
puntuacion: score,
comentario: comment,
etiquetas: ['Puntualidad ⏱️', 'Servicio Seguro 🛡️', 'Verificado ✅']
})
});
if (res.ok) {
formReview.reset();
if (modalReview) modalReview.classList.remove('active');
fetchStatsAndReviews();
alert('¡Gracias por tu calificación! Ha sido agregada al muro de la comunidad.');
}
} catch (err) {
console.warn('Error al enviar reseña:', err);
} finally {
btnSubmit.disabled = false;
btnSubmit.innerHTML = '⭐ Publicar Mi Calificación';
}
});
}
}
async function fetchStatsAndReviews() {
try {
const res = await fetch('/api/stats-reviews');
if (res.ok) {
const data = await res.json();
if (data.stats?.global) {
const g = data.stats.global;
const totalViajesEl = document.getElementById('stat-total-viajes');
const ratingAvgEl = document.getElementById('stat-rating-avg');
const puntualidadEl = document.getElementById('stat-puntualidad');
const conductoresActivosEl = document.getElementById('stat-conductores-activos');
if (totalViajesEl) totalViajesEl.textContent = `${g.totalViajes}+`;
if (ratingAvgEl) ratingAvgEl.textContent = `${g.calificacionPromedio} / 5.0`;
if (puntualidadEl) puntualidadEl.textContent = g.indicePuntualidad;
if (conductoresActivosEl) conductoresActivosEl.textContent = g.conductoresCertificados;
}
if (data.recentReviews && Array.isArray(data.recentReviews)) {
renderReviewsGrid(data.recentReviews);
}
}
} catch (err) {
console.warn('⚠️ [Stats/Reviews Error]:', err);
}
}
function renderReviewsGrid(reviews) {
const container = document.getElementById('reviews-list-grid');
if (!container) return;
container.innerHTML = reviews.map(r => {
const starsCount = Math.round(r.puntuacion || 5);
const starsString = '★'.repeat(starsCount) + '☆'.repeat(5 - starsCount);
const tagsHtml = (r.etiquetas || []).map(t => `${t} `).join('');
const isPasajero = r.autorRol === 'PASAJERO';
return `
Ruta: ${r.ruta || 'Cali ➔ Buenaventura'} • Calificó a: ${r.destinatarioNombre}
"${r.comentario}"
${tagsHtml}
`;
}).join('');
}
/* ==========================================================================
6. CONFIGURACIÓN DE TARIFAS Y CANAL DE ATENCIÓN WHATSAPP
========================================================================== */
async function fetchTariffs() {
try {
const res = await fetch('/api/tariffs');
if (res.ok) {
const data = await res.json();
if (data.config) {
currentTariffs = { ...currentTariffs, ...data.config };
syncTariffsToUI();
}
}
} catch (err) {
console.warn('⚠️ [Tarifas API]: Modo local/offline activo, utilizando tarifas por defecto.', err);
syncTariffsToUI();
}
}
function syncTariffsToUI() {
// 1. Formulario administrativo de tarifas y canales oficiales
const elPasajero = document.getElementById('cfg-tarifa-pasajero');
const elBasePaquete = document.getElementById('cfg-base-paquete');
const elKmPaquete = document.getElementById('cfg-km-paquete');
const elSobre = document.getElementById('cfg-factor-sobre');
const elMediano = document.getElementById('cfg-factor-mediano');
const elGrande = document.getElementById('cfg-factor-grande');
const elSeguro = document.getElementById('cfg-seguro-base');
const elComision = document.getElementById('cfg-comision');
const elPenalidad = document.getElementById('cfg-penalidad');
const elRadio = document.getElementById('cfg-radio-pooling');
const elPhone = document.getElementById('cfg-admin-phone');
const elName = document.getElementById('cfg-admin-name');
// Canales de WhatsApp y separación de grupos por App
const elGroupPacific = document.getElementById('cfg-group-pacifictrans');
const elGroupDripzone = document.getElementById('cfg-group-dripzone');
const adminWaGroupBadge = document.getElementById('admin-wa-group-badge');
if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija;
if (elBasePaquete) elBasePaquete.value = currentTariffs.precioBasePaquete;
if (elKmPaquete) elKmPaquete.value = currentTariffs.precioKmPaquete;
if (elSobre && currentTariffs.factorPaquetes) elSobre.value = currentTariffs.factorPaquetes.sobre || 1.0;
if (elMediano && currentTariffs.factorPaquetes) elMediano.value = currentTariffs.factorPaquetes.mediano || 1.6;
if (elGrande && currentTariffs.factorPaquetes) elGrande.value = currentTariffs.factorPaquetes.grande || 2.2;
if (elSeguro) elSeguro.value = currentTariffs.seguroBasePaquete;
if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin;
if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion;
if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros;
if (elPhone && currentTariffs.numeroAtencionWhatsApp) elPhone.value = currentTariffs.numeroAtencionWhatsApp;
if (elName && currentTariffs.nombreSoporte) elName.value = currentTariffs.nombreSoporte;
const pacificGroup = currentTariffs.grupoWhatsAppPacificTrans || 'Pacific Trans Notificaciones';
const dripGroup = currentTariffs.grupoWhatsAppDripzone || 'Dripzone Notificaciones';
if (elGroupPacific) elGroupPacific.value = pacificGroup;
if (elGroupDripzone) elGroupDripzone.value = dripGroup;
if (adminWaGroupBadge) adminWaGroupBadge.textContent = `👥 Grupo: ${pacificGroup}`;
// 2. Sincronización del enlace dinámico de atención WhatsApp en el footer y navbar
const phoneFormatted = formatPhoneNumber(currentTariffs.numeroAtencionWhatsApp || '573167455877');
const cleanPhone = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, '');
const footerPhoneEl = document.getElementById('footer-support-phone');
const footerLinkEl = document.getElementById('footer-support-link');
if (footerPhoneEl) footerPhoneEl.textContent = phoneFormatted;
if (footerLinkEl) footerLinkEl.href = `https://wa.me/${cleanPhone}?text=Hola%20Pacifictrans%20Go,%20deseo%20informaci%C3%B3n%20sobre%20un%20servicio`;
// 3. Precios en módulo de reservas de pasajeros
if (window.updatePassengerBookingSummary) {
window.updatePassengerBookingSummary();
} else {
const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`;
const totalPriceEl = document.querySelector('.total-price');
if (totalPriceEl) totalPriceEl.textContent = formattedPasajero;
}
// 4. Recalcular módulo de mensajería y fletes
if (window.recalculateFreight) {
window.recalculateFreight();
}
}
function formatPhoneNumber(num) {
const clean = num.replace(/\D/g, '');
if (clean.startsWith('57') && clean.length === 12) {
return `+57 ${clean.slice(2, 5)} ${clean.slice(5, 8)} ${clean.slice(8)}`;
}
return `+${clean}`;
}
function initTariffsManager() {
const form = document.getElementById('form-tariffs-config');
const statusEl = document.getElementById('tariffs-save-status');
const btnDetect = document.getElementById('btn-detect-wa-groups');
// Auto-Detección de Grupos WhatsApp mediante Evolution API
if (btnDetect) {
btnDetect.addEventListener('click', async () => {
btnDetect.disabled = true;
const originalText = btnDetect.innerHTML;
btnDetect.innerHTML = '⏳ Detectando grupos...';
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/admin/whatsapp/groups', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await res.json();
if (res.ok && data.success) {
const inputPacific = document.getElementById('cfg-group-pacifictrans');
const inputDrip = document.getElementById('cfg-group-dripzone');
const badgePacific = document.getElementById('badge-pacific-jid');
const badgeDrip = document.getElementById('badge-dripzone-jid');
if (inputPacific && data.pacificTrans?.groupName) {
inputPacific.value = data.pacificTrans.groupName;
}
if (badgePacific && data.pacificTrans?.resolvedJid) {
badgePacific.innerHTML = `✓ Destino: ${data.pacificTrans.resolvedJid} (${data.pacificTrans.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`;
badgePacific.style.color = '#34d399';
}
if (inputDrip && data.dripzone?.groupName) {
inputDrip.value = data.dripzone.groupName;
}
if (badgeDrip && data.dripzone?.resolvedJid) {
badgeDrip.innerHTML = `✓ Destino: ${data.dripzone.resolvedJid} (${data.dripzone.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`;
badgeDrip.style.color = '#34d399';
}
alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacifictrans Go ➔ ${data.pacificTrans?.groupName} (${data.pacificTrans?.resolvedJid})\n• Dripzone ➔ ${data.dripzone?.groupName} (${data.dripzone?.resolvedJid})`);
} else {
alert('No se pudieron consultar los grupos: ' + (data.error || 'Verifica la sesión'));
}
} catch (err) {
console.error('Error detect groups:', err);
alert('Error al consultar Evolution API');
} finally {
btnDetect.disabled = false;
btnDetect.innerHTML = originalText;
}
});
}
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btn-save-tariffs');
if (btn) {
btn.disabled = true;
btn.innerHTML = '⏳ Guardando en MongoDB...';
}
const payload = {
tarifaPasajeroFija: parseFloat(document.getElementById('cfg-tarifa-pasajero')?.value || '35000'),
precioBasePaquete: parseFloat(document.getElementById('cfg-base-paquete')?.value || '25000'),
precioKmPaquete: parseFloat(document.getElementById('cfg-km-paquete')?.value || '1200'),
factorPaquetes: {
sobre: parseFloat(document.getElementById('cfg-factor-sobre')?.value || '1.0'),
mediano: parseFloat(document.getElementById('cfg-factor-mediano')?.value || '1.6'),
grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2')
},
seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'),
porcentajeComisionAdmin: parseFloat(document.getElementById('cfg-comision')?.value || '15'),
costoPenalidadCancelacion: parseFloat(document.getElementById('cfg-penalidad')?.value || '15000'),
radioMaxPoolingMetros: parseFloat(document.getElementById('cfg-radio-pooling')?.value || '4000'),
numeroAtencionWhatsApp: document.getElementById('cfg-admin-phone')?.value.trim() || '573167455877',
nombreSoporte: document.getElementById('cfg-admin-name')?.value.trim() || 'Atención Pacifictrans Go',
grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans Notificaciones',
grupoWhatsAppDripzone: document.getElementById('cfg-group-dripzone')?.value.trim() || 'Dripzone Notificaciones'
};
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/tariffs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (res.ok) {
const data = await res.json();
currentTariffs = { ...currentTariffs, ...data.config };
syncTariffsToUI();
if (statusEl) {
statusEl.innerHTML = `✅ ¡Configuración guardada y sincronizada! Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Grupo Pacific: "${payload.grupoWhatsAppPacificTrans}" | Grupo Dripzone: "${payload.grupoWhatsAppDripzone}"`;
statusEl.style.color = '#10b981';
}
} else {
throw new Error('Error al guardar en el servidor');
}
} catch (err) {
currentTariffs = { ...currentTariffs, ...payload };
syncTariffsToUI();
if (statusEl) {
statusEl.innerHTML = `✅ Configuración actualizada localmente. `;
statusEl.style.color = '#10b981';
}
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '💾 Guardar y Aplicar Configuración';
}
}
});
}
/* ==========================================================================
7. RESERVA DE PASAJEROS & POOLING
========================================================================== */
function initPassengerCalculator() {
const originSelect = document.getElementById('pool-origin');
const addressGroup = document.getElementById('cali-address-group');
const buenaStopGroup = document.getElementById('buena-stop-group');
const caliAddressInput = document.getElementById('cali-address');
const buenaStopSelect = document.getElementById('buena-stop');
const bookBtn = document.getElementById('btn-book-pool');
const poolDots = document.querySelectorAll('.seat-dot');
const passengersContainer = document.getElementById('passengers-cards-container');
const badgeCupos = document.getElementById('badge-cupos-selected');
const seatButtons = document.querySelectorAll('.btn-qty-seat');
const paymentCards = document.querySelectorAll('.payment-method-card');
let selectedSeats = 1;
let selectedPaymentMethod = 'BOLD';
let currentSeatsFilled = 3;
// Cache para persistir los datos ingresados al alternar entre 1..4 cupos
const passengersData = [
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' },
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' },
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' },
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }
];
function saveCurrentFormData() {
for (let i = 1; i <= selectedSeats; i++) {
const nameInput = document.getElementById(`passenger-${i}-name`);
const docInput = document.getElementById(`passenger-${i}-doc`);
const phoneInput = document.getElementById(`passenger-${i}-phone`);
const notesInput = document.getElementById(`passenger-${i}-notes`);
const sameAddrCheck = document.getElementById(`passenger-${i}-same-addr`);
const addrInput = document.getElementById(`passenger-${i}-addr`);
if (nameInput) passengersData[i - 1].nombre = nameInput.value;
if (docInput) passengersData[i - 1].doc = docInput.value;
if (phoneInput) passengersData[i - 1].phone = phoneInput.value;
if (notesInput) passengersData[i - 1].notes = notesInput.value;
if (sameAddrCheck) passengersData[i - 1].sameAddress = sameAddrCheck.checked;
if (addrInput) passengersData[i - 1].address = addrInput.value;
}
}
function renderPassengerCards(count) {
if (!passengersContainer) return;
// Si hay usuario logueado y el pasajero 1 no tiene nombre, prellenar
if (currentUser && !passengersData[0].nombre) {
passengersData[0].nombre = currentUser.nombre || '';
passengersData[0].phone = currentUser.telefono || '';
}
let html = '';
for (let i = 1; i <= count; i++) {
const isTitular = (i === 1);
const data = passengersData[i - 1];
html += `
`;
}
passengersContainer.innerHTML = html;
// Conectar eventos a los checkboxes de misma dirección
for (let i = 2; i <= count; i++) {
const check = document.getElementById(`passenger-${i}-same-addr`);
const addrGroup = document.getElementById(`passenger-${i}-addr-group`);
if (check && addrGroup) {
check.addEventListener('change', () => {
addrGroup.style.display = check.checked ? 'none' : 'block';
passengersData[i - 1].sameAddress = check.checked;
});
}
}
}
let appliedCoupon = null;
const poolCouponInput = document.getElementById('pool-coupon-code');
const btnApplyPoolCoupon = document.getElementById('btn-apply-pool-coupon');
const poolCouponFeedback = document.getElementById('pool-coupon-feedback');
if (btnApplyPoolCoupon && poolCouponInput) {
btnApplyPoolCoupon.addEventListener('click', async () => {
const code = poolCouponInput.value.trim().toUpperCase();
if (!code) {
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
poolCouponFeedback.textContent = '⚠️ Ingresa el código del bono o cupón.';
poolCouponFeedback.style.display = 'flex';
}
return;
}
btnApplyPoolCoupon.disabled = true;
btnApplyPoolCoupon.textContent = '...';
try {
const subtotal = selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000);
const res = await fetch('/api/coupons/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ codigo: code, serviceType: 'VIAJES', subtotal })
});
const data = await res.json();
if (data.valid) {
appliedCoupon = data;
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-success';
poolCouponFeedback.textContent = data.message;
poolCouponFeedback.style.display = 'flex';
}
} else {
appliedCoupon = null;
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
poolCouponFeedback.textContent = data.message || 'Cupón inválido';
poolCouponFeedback.style.display = 'flex';
}
}
updateSummary();
} catch (err) {
console.error('Coupon validation error:', err);
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
poolCouponFeedback.textContent = 'Error al validar cupón con el servidor.';
poolCouponFeedback.style.display = 'flex';
}
} finally {
btnApplyPoolCoupon.disabled = false;
btnApplyPoolCoupon.textContent = 'Aplicar';
}
});
}
function updateSummary() {
const unitPrice = currentTariffs.tarifaPasajeroFija || 35000;
const subtotal = selectedSeats * unitPrice;
let discountAmount = 0;
if (appliedCoupon && appliedCoupon.valid) {
if (appliedCoupon.coupon.tipoDescuento === 'PORCENTAJE') {
discountAmount = Math.round(subtotal * (appliedCoupon.coupon.valorDescuento / 100));
} else {
discountAmount = Math.min(appliedCoupon.coupon.valorDescuento, subtotal);
}
discountAmount = Math.min(discountAmount, subtotal);
}
const totalAmount = Math.max(0, subtotal - discountAmount);
const formattedUnit = `$${unitPrice.toLocaleString('es-CO')} COP`;
const formattedSubtotal = `$${subtotal.toLocaleString('es-CO')} COP`;
const formattedTotal = `$${totalAmount.toLocaleString('es-CO')} COP`;
const summarySubtotalRow = document.getElementById('summary-subtotal-row');
const summarySubtotalPrice = document.getElementById('summary-subtotal-price');
const summaryDiscountRow = document.getElementById('summary-discount-row');
const summaryDiscountAmount = document.getElementById('summary-discount-amount');
const summaryPriceLabel = document.getElementById('summary-price-label');
const summaryTotalPrice = document.getElementById('summary-total-price');
const summaryCapacity = document.getElementById('summary-capacity-text');
const summarySeatsPill = document.getElementById('summary-seats-pill');
const btnBookText = document.getElementById('btn-book-text');
const badgeCuposSelected = document.getElementById('badge-cupos-selected');
if (badgeCuposSelected) {
badgeCuposSelected.textContent = `${selectedSeats} ${selectedSeats === 1 ? 'Cupo Seleccionado' : 'Cupos Seleccionados'}`;
}
if (summarySeatsPill) {
summarySeatsPill.textContent = `${selectedSeats} ${selectedSeats === 1 ? 'Pasajero (1 cupo)' : `Pasajeros (${selectedSeats} cupos)`}`;
}
if (summaryCapacity) {
summaryCapacity.textContent = selectedSeats === 4
? 'Vehículo Completo Exclusivo (4/4 Cupos)'
: `Máximo 4 Pasajeros (${selectedSeats} ${selectedSeats === 1 ? 'cupo solicitado' : 'cupos solicitados'})`;
}
if (discountAmount > 0) {
if (summarySubtotalRow) {
summarySubtotalRow.style.display = 'flex';
if (summarySubtotalPrice) summarySubtotalPrice.textContent = formattedSubtotal;
}
if (summaryDiscountRow) {
summaryDiscountRow.style.display = 'flex';
const typeText = appliedCoupon.coupon.tipoDescuento === 'PORCENTAJE' ? `${appliedCoupon.coupon.valorDescuento}% OFF` : 'Bono Fijo';
if (summaryDiscountAmount) summaryDiscountAmount.textContent = `- $${discountAmount.toLocaleString('es-CO')} COP (${typeText})`;
}
if (summaryPriceLabel) {
summaryPriceLabel.textContent = `Total con Descuento (${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'}):`;
}
} else {
if (summarySubtotalRow) summarySubtotalRow.style.display = 'none';
if (summaryDiscountRow) summaryDiscountRow.style.display = 'none';
if (summaryPriceLabel) {
summaryPriceLabel.textContent = `Total a Pagar (${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'}):`;
}
}
if (summaryTotalPrice) {
summaryTotalPrice.textContent = formattedTotal;
}
if (btnBookText) {
const methodText = selectedPaymentMethod === 'BOLD' ? 'Pagar con Bold' : (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar');
btnBookText.textContent = `${methodText} ${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'} (${formattedTotal})`;
}
// Actualizar previsualización en dots del vehículo
updateSeatVisuals(selectedSeats);
}
window.updatePassengerBookingSummary = updateSummary;
// Escuchadores de botones de selección de cupos
seatButtons.forEach((btn) => {
btn.addEventListener('click', () => {
saveCurrentFormData();
seatButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedSeats = parseInt(btn.getAttribute('data-seats') || '1', 10);
renderPassengerCards(selectedSeats);
updateSummary();
});
});
// Selector de método de pago
paymentCards.forEach((card) => {
card.addEventListener('click', () => {
paymentCards.forEach(c => c.classList.remove('active'));
card.classList.add('active');
const radio = card.querySelector('input[type="radio"]');
if (radio) {
radio.checked = true;
selectedPaymentMethod = radio.value;
}
updateSummary();
});
});
function updateOriginDestination() {
if (!originSelect) return;
const isCali = originSelect.value === 'CALI';
if (addressGroup) addressGroup.style.display = isCali ? 'block' : 'none';
if (buenaStopGroup) buenaStopGroup.style.display = isCali ? 'block' : 'none';
}
if (originSelect) {
originSelect.addEventListener('change', updateOriginDestination);
updateOriginDestination();
}
// Render inicial
renderPassengerCards(selectedSeats);
updateSummary();
// Envío del Formulario y Reserva en MongoDB con Pooling
if (bookBtn) {
bookBtn.addEventListener('click', async (e) => {
e.preventDefault();
saveCurrentFormData();
// 1. Validar dirección principal de Cali
const caliAddress = caliAddressInput ? caliAddressInput.value.trim() : '';
const isCaliOrigin = originSelect ? originSelect.value === 'CALI' : true;
if (isCaliOrigin && !caliAddress) {
alert('📍 Por favor ingresa la dirección de recogida en Cali (domicilio puerta a puerta).');
caliAddressInput?.focus();
return;
}
// 2. Validar que cada pasajero tenga nombre, documento y teléfono
const validatedPassengers = [];
for (let i = 1; i <= selectedSeats; i++) {
const d = passengersData[i - 1];
if (!d.nombre || d.nombre.trim().length < 3) {
alert(`⚠️ Por favor escribe el nombre completo del Pasajero #${i}.`);
document.getElementById(`passenger-${i}-name`)?.focus();
return;
}
if (!d.doc || d.doc.trim().length < 5) {
alert(`⚠️ Por favor ingresa el documento de identidad del Pasajero #${i} (requerido para la póliza contractual SOAT).`);
document.getElementById(`passenger-${i}-doc`)?.focus();
return;
}
if (!d.phone || d.phone.trim().replace(/\D/g, '').length < 7) {
alert(`⚠️ Por favor ingresa un número de teléfono o WhatsApp válido para el Pasajero #${i}.`);
document.getElementById(`passenger-${i}-phone`)?.focus();
return;
}
const pAddress = (!d.sameAddress && d.address && d.address.trim())
? d.address.trim()
: (caliAddress || 'Punto Oficial Buenaventura');
validatedPassengers.push({
nombre: d.nombre.trim(),
documentoIdentidad: d.doc.trim(),
telefono: d.phone.trim(),
direccionRecogida: pAddress,
coordenadas: [-76.5320, 3.4215],
notas: d.notes ? d.notes.trim() : '',
esTitular: (i === 1)
});
}
// 3. Verificación de seguridad OTP
if (!currentUser) {
const p1Phone = validatedPassengers[0].telefono.replace(/\D/g, '');
const authInput = document.getElementById('auth-identifier');
if (authInput && p1Phone) {
authInput.value = p1Phone;
}
alert('🛡️ Por seguridad de los conductores y de los demás pasajeros, debes verificar tu identidad con OTP antes de confirmar tu reserva.');
document.getElementById('modal-auth-dripzone')?.classList.add('active');
return;
}
// 4. Procesar reserva en el Backend
bookBtn.disabled = true;
const originalBtnHtml = bookBtn.innerHTML;
bookBtn.innerHTML = `
Agrupando ${selectedSeats} Cupo(s)...
`;
try {
const payload = {
usuarioId: currentUser ? currentUser._id : undefined,
sentido: isCaliOrigin ? 'CALI_A_BUENAVENTURA' : 'BUENAVENTURA_A_CALI',
direccionTexto: caliAddress || 'Terminal Buenaventura',
coordenadas: [-76.5320, 3.4215],
puntoFijoBuenaventura: {
nombre: buenaStopSelect ? buenaStopSelect.options[buenaStopSelect.selectedIndex].text : 'Terminal de Transporte de Buenaventura',
coordenadas: [-77.0250, 3.8820]
},
metodoPago: selectedPaymentMethod,
cuposSolicitados: selectedSeats,
pasajeros: validatedPassengers,
codigoCupon: appliedCoupon?.coupon?.codigo || undefined,
email: currentUser?.email || ''
};
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/trips/pool', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
const totalOccupied = data.totalCuposOcupados || data.pasajerosAgrupados || 4;
updateSeatVisuals(totalOccupied);
// Mostrar alerta detallada
const alertBox = document.getElementById('pool-dispatch-alert');
const alertTitle = document.getElementById('alert-title');
const alertMsg = document.getElementById('alert-message');
const alertBreakdown = document.getElementById('alert-passengers-breakdown');
const alertBoldArea = document.getElementById('alert-bold-payment-area');
if (alertTitle) {
alertTitle.textContent = totalOccupied === 4
? '🔥 ¡Vehículo Completo (4/4 Cupos) Listo para Despacho!'
: `✅ ¡${selectedSeats} ${selectedSeats === 1 ? 'Cupo Reservado' : 'Cupos Reservados'} Exitosamente! (${totalOccupied}/4 Ocupados)`;
}
if (alertMsg) {
alertMsg.innerHTML = `
Viaje #${data.tripId?.slice(-6) || ''} en ruta ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}.
${data.message}
`;
}
if (alertBreakdown) {
let breakdownHtml = `
👥 Pasajeros Registrados en esta Reserva:
`;
validatedPassengers.forEach((p, idx) => {
breakdownHtml += `
${idx + 1}. ${p.nombre} (C.C. ${p.documentoIdentidad}) • Tel: +${p.telefono} • Recogida: ${p.direccionRecogida} ${p.notas ? `• 🧳 ${p.notas}` : ''}
`;
});
breakdownHtml += `
`;
alertBreakdown.innerHTML = breakdownHtml;
}
// Si es pago Bold, renderizar botón y QR
if (alertBoldArea) {
if (data.boldPayment && data.boldPayment.checkoutUrl) {
alertBoldArea.style.display = 'block';
alertBoldArea.innerHTML = `
`;
} else {
alertBoldArea.style.display = 'none';
}
}
if (alertBox) {
alertBox.style.display = 'block';
alertBox.scrollIntoView({ behavior: 'smooth' });
}
const formattedTotal = `$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP`;
bookBtn.innerHTML = `✅ ${selectedSeats} Cupo(s) Confirmado(s) (${formattedTotal})`;
} else {
alert('❌ No se pudo completar la reserva: ' + (data.message || data.error || 'Intenta de nuevo'));
bookBtn.innerHTML = originalBtnHtml;
bookBtn.disabled = false;
}
} catch (err) {
console.error('Error booking pool:', err);
alert('⚠️ Error de conexión con el servidor de pooling. Verifica tu red.');
bookBtn.innerHTML = originalBtnHtml;
bookBtn.disabled = false;
}
});
}
function updateSeatVisuals(count) {
poolDots.forEach((dot, index) => {
dot.classList.remove('active', 'pending');
if (index < count) {
dot.classList.add('active');
} else if (index === count) {
dot.classList.add('pending');
}
});
const seatCounter = document.getElementById('seat-counter-text');
if (seatCounter) seatCounter.textContent = `${count}/4 Pasajeros Confirmados`;
}
updateSeatVisuals(currentSeatsFilled);
}
/* ==========================================================================
8. CALCULADORA DE MENSAJERÍA & FLETES
========================================================================== */
function initFreightCalculator() {
const packageTypeSelect = document.getElementById('package-type');
const declaredValueInput = document.getElementById('declared-value');
const totalPriceEl = document.getElementById('freight-total-price');
const baseCostEl = document.getElementById('freight-base-cost');
const distCostEl = document.getElementById('freight-dist-cost');
const insuranceCostEl = document.getElementById('freight-insurance-cost');
const freightSubtotalRow = document.getElementById('freight-subtotal-row');
const freightSubtotalPrice = document.getElementById('freight-subtotal-price');
const freightDiscountRow = document.getElementById('freight-discount-row');
const freightDiscountAmount = document.getElementById('freight-discount-amount');
const freightCouponInput = document.getElementById('freight-coupon-code');
const btnApplyFreightCoupon = document.getElementById('btn-apply-freight-coupon');
const freightCouponFeedback = document.getElementById('freight-coupon-feedback');
const btnBookFreight = document.getElementById('btn-book-freight');
const KM_DIST = 115; // Corredor Cali - Buenaventura
let appliedFreightCoupon = null;
window.recalculateFreight = function () {
const rawFactor = parseFloat(packageTypeSelect?.value || '1.0');
let factor = rawFactor;
if (rawFactor === 1.0 && currentTariffs.factorPaquetes?.sobre) {
factor = currentTariffs.factorPaquetes.sobre;
} else if (rawFactor === 1.6 && currentTariffs.factorPaquetes?.mediano) {
factor = currentTariffs.factorPaquetes.mediano;
} else if (rawFactor === 2.2 && currentTariffs.factorPaquetes?.grande) {
factor = currentTariffs.factorPaquetes.grande;
}
const declaredValue = parseFloat(declaredValueInput?.value || '0');
const basePrice = currentTariffs.precioBasePaquete || 25000;
const costPerKm = currentTariffs.precioKmPaquete || 1200;
const distancePart = KM_DIST * costPerKm;
const subtotal = (basePrice + distancePart) * factor;
const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0);
const subtotalBruto = Math.round(subtotal + seguro);
let discountAmount = 0;
if (appliedFreightCoupon && appliedFreightCoupon.valid) {
if (appliedFreightCoupon.coupon.tipoDescuento === 'PORCENTAJE') {
discountAmount = Math.round(subtotalBruto * (appliedFreightCoupon.coupon.valorDescuento / 100));
} else {
discountAmount = Math.min(appliedFreightCoupon.coupon.valorDescuento, subtotalBruto);
}
discountAmount = Math.min(discountAmount, subtotalBruto);
}
const total = Math.max(0, subtotalBruto - discountAmount);
if (baseCostEl) baseCostEl.textContent = `$${basePrice.toLocaleString('es-CO')} COP`;
if (distCostEl) distCostEl.textContent = `$${Math.round(distancePart * factor).toLocaleString('es-CO')} COP`;
if (insuranceCostEl) insuranceCostEl.textContent = `$${seguro.toLocaleString('es-CO')} COP`;
if (discountAmount > 0) {
if (freightSubtotalRow) {
freightSubtotalRow.style.display = 'flex';
if (freightSubtotalPrice) freightSubtotalPrice.textContent = `$${subtotalBruto.toLocaleString('es-CO')} COP`;
}
if (freightDiscountRow) {
freightDiscountRow.style.display = 'flex';
const typeText = appliedFreightCoupon.coupon.tipoDescuento === 'PORCENTAJE' ? `${appliedFreightCoupon.coupon.valorDescuento}% OFF` : 'Bono Fijo';
if (freightDiscountAmount) freightDiscountAmount.textContent = `- $${discountAmount.toLocaleString('es-CO')} COP (${typeText})`;
}
} else {
if (freightSubtotalRow) freightSubtotalRow.style.display = 'none';
if (freightDiscountRow) freightDiscountRow.style.display = 'none';
}
if (totalPriceEl) totalPriceEl.textContent = `$${total.toLocaleString('es-CO')} COP`;
};
if (packageTypeSelect) packageTypeSelect.addEventListener('change', window.recalculateFreight);
if (declaredValueInput) declaredValueInput.addEventListener('input', window.recalculateFreight);
if (btnApplyFreightCoupon && freightCouponInput) {
btnApplyFreightCoupon.addEventListener('click', async () => {
const code = freightCouponInput.value.trim().toUpperCase();
if (!code) {
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
freightCouponFeedback.textContent = '⚠️ Ingresa el código del bono o cupón de envío.';
freightCouponFeedback.style.display = 'flex';
}
return;
}
btnApplyFreightCoupon.disabled = true;
btnApplyFreightCoupon.textContent = '...';
try {
const rawFactor = parseFloat(packageTypeSelect?.value || '1.0');
const declaredValue = parseFloat(declaredValueInput?.value || '0');
const basePrice = currentTariffs.precioBasePaquete || 25000;
const costPerKm = currentTariffs.precioKmPaquete || 1200;
const distancePart = KM_DIST * costPerKm;
const subtotal = (basePrice + distancePart) * rawFactor;
const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0);
const subtotalBruto = Math.round(subtotal + seguro);
const res = await fetch('/api/coupons/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ codigo: code, serviceType: 'ENCOMIENDAS', subtotal: subtotalBruto })
});
const data = await res.json();
if (data.valid) {
appliedFreightCoupon = data;
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-success';
freightCouponFeedback.textContent = data.message;
freightCouponFeedback.style.display = 'flex';
}
} else {
appliedFreightCoupon = null;
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
freightCouponFeedback.textContent = data.message || 'Cupón no válido para encomiendas';
freightCouponFeedback.style.display = 'flex';
}
}
window.recalculateFreight();
} catch (err) {
console.error('Coupon freight error:', err);
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
freightCouponFeedback.textContent = 'Error al verificar cupón.';
freightCouponFeedback.style.display = 'flex';
}
} finally {
btnApplyFreightCoupon.disabled = false;
btnApplyFreightCoupon.textContent = 'Aplicar';
}
});
}
// Despacho / Creación de Encomienda con Bold y Cupón
if (btnBookFreight) {
btnBookFreight.addEventListener('click', async (e) => {
e.preventDefault();
const senderPhone = document.getElementById('sender-phone')?.value.trim();
const receiverPhone = document.getElementById('receiver-phone')?.value.trim();
const packageDesc = document.getElementById('package-desc')?.value.trim();
if (!senderPhone || senderPhone.replace(/\D/g, '').length < 7) {
alert('⚠️ Por favor ingresa el teléfono/WhatsApp del remitente.');
document.getElementById('sender-phone')?.focus();
return;
}
if (!receiverPhone || receiverPhone.replace(/\D/g, '').length < 7) {
alert('⚠️ Por favor ingresa el teléfono/WhatsApp del destinatario.');
document.getElementById('receiver-phone')?.focus();
return;
}
if (!packageDesc) {
alert('⚠️ Por favor describe el contenido de la encomienda.');
document.getElementById('package-desc')?.focus();
return;
}
btnBookFreight.disabled = true;
const originalText = btnBookFreight.innerHTML;
btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Bold...';
try {
const payload = {
tipoPaquete: packageTypeSelect ? packageTypeSelect.options[packageTypeSelect.selectedIndex].text : 'Paquete Pequeño',
factor: parseFloat(packageTypeSelect?.value || '1.2'),
valorDeclarado: parseFloat(declaredValueInput?.value || '100000'),
telefonoRemitente: senderPhone,
telefonoDestinatario: receiverPhone,
descripcionContenido: packageDesc,
codigoCupon: appliedFreightCoupon?.coupon?.codigo || undefined
};
const res = await fetch('/api/trips/package', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (data.boldPaymentLink) {
window.open(data.boldPaymentLink, '_blank');
alert(`✅ ¡Guía #${data.guiaSeguimiento || 'PT'} generada!\n\nTarifa Liquidada: $${(data.tarifaFinal || data.tarifaCalculada).toLocaleString('es-CO')} COP\n${data.descuentoCupon ? '🎟️ Descuento aplicado: -$' + data.descuentoCupon.descuento.toLocaleString('es-CO') + ' COP\n' : ''}\nSe ha abierto la pasarela Bold Smart Checkout para completar el pago.`);
} else {
alert(`✅ Guía generada correctamente: #${data.guiaSeguimiento}`);
}
} else {
alert('Error al generar envío: ' + (data.error || 'Verifica los datos'));
}
} catch (err) {
console.error('Error package booking:', err);
alert('Error al conectar con el servidor.');
} finally {
btnBookFreight.disabled = false;
btnBookFreight.innerHTML = originalText;
}
});
}
}
/* ==========================================================================
9. CENTRO DE NOTIFICACIONES & CONTADOR DE MENSAJES PENDIENTES
========================================================================== */
function initNotificationsSystem() {
const btnBell = document.getElementById('btn-toggle-notifs');
const dropdown = document.getElementById('notif-dropdown-panel');
const badge = document.getElementById('notif-unread-count');
const headerUnread = document.getElementById('notif-header-unread');
const listEl = document.getElementById('notif-items-list');
const btnMarkAll = document.getElementById('btn-mark-all-read');
const waLink = document.getElementById('notif-wa-link');
const defaultNotifs = [
{
id: '1',
icon: '🚗',
title: '¡Cupo 4/4 Completado!',
desc: 'Tu viaje Cali ➔ Buenaventura tiene conductor asignado (Mario Fernando G.). Recogida escalonada y ruta Waze TSP activa.',
time: 'Hace 5 min',
unread: true
},
{
id: '2',
icon: '📦',
title: 'Encomienda en Ruta 40',
desc: 'Guía #PT-8921 despachada desde Cali con destino a Buenaventura. Seguimiento satelital en vivo.',
time: 'Hace 25 min',
unread: true
},
{
id: '3',
icon: '🛡️',
title: 'Identidad Verificada 2FA',
desc: 'Tu cuenta ha sido validada con WhatsApp OTP. Viajes y envíos 100% seguros y respaldados.',
time: 'Hace 1 hora',
unread: true
}
];
let stored = localStorage.getItem('pt_notifications');
let notifications = stored ? JSON.parse(stored) : defaultNotifs;
function saveAndRender() {
localStorage.setItem('pt_notifications', JSON.stringify(notifications));
const unreadCount = notifications.filter(n => n.unread).length;
if (badge) {
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? 'flex' : 'none';
}
if (headerUnread) {
headerUnread.textContent = `${unreadCount} nuevas`;
headerUnread.style.display = unreadCount > 0 ? 'inline-block' : 'none';
}
if (listEl) {
if (notifications.length === 0) {
listEl.innerHTML = 'No tienes notificaciones pendientes.
';
} else {
listEl.innerHTML = notifications.map(n => `
${n.icon}
${n.title}
${n.desc}
${n.time}
`).join('');
listEl.querySelectorAll('.notif-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.getAttribute('data-id');
const target = notifications.find(n => n.id === id);
if (target) {
target.unread = false;
saveAndRender();
}
});
});
}
}
}
if (btnBell && dropdown) {
btnBell.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.classList.toggle('active');
});
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target) && !btnBell.contains(e.target)) {
dropdown.classList.remove('active');
}
});
}
if (btnMarkAll) {
btnMarkAll.addEventListener('click', () => {
notifications.forEach(n => { n.unread = false; });
saveAndRender();
});
}
// Sincronizar enlace de WhatsApp con el número configurado dinámicamente
if (waLink && currentTariffs.numeroAtencionWhatsApp) {
const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, '');
waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacifictrans%20Go`;
}
saveAndRender();
}
/* ==========================================================================
10. CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS INMEDIATAS BRE-B
========================================================================== */
function initSettlementsAndBreBSystem() {
const defaultDriverId = '6528f1100000000000000001';
let activeTripData = null;
let currentDriverWallet = null;
// Modal elements
const brebModal = document.getElementById('modal-breb-receipt');
const btnCloseBrebModal = document.getElementById('btn-close-breb-modal');
const btnCopyBrebRef = document.getElementById('btn-copy-breb-receipt');
const btnShareBrebWa = document.getElementById('btn-share-breb-whatsapp');
// Helper para abrir el comprobante oficial de Bre-B
window.openBreBReceiptModal = function (receipt) {
if (!brebModal) return;
const amountEl = document.getElementById('modal-receipt-amount');
const datetimeEl = document.getElementById('modal-receipt-datetime');
const refEl = document.getElementById('modal-receipt-ref');
const authEl = document.getElementById('modal-receipt-auth');
const cycleEl = document.getElementById('modal-receipt-cycle');
const bankEl = document.getElementById('modal-receipt-bank');
const keyEl = document.getElementById('modal-receipt-key');
const ownerEl = document.getElementById('modal-receipt-owner');
if (amountEl) amountEl.textContent = `$${(receipt.monto || 0).toLocaleString('es-CO')} COP`;
if (datetimeEl) datetimeEl.textContent = receipt.fechaHora || new Date().toLocaleString('es-CO');
if (refEl) refEl.textContent = receipt.referenciaBreB || 'BREB-CO-20261014-94F2B0';
if (authEl) authEl.textContent = receipt.codigoAutorizacion || 'AUT-98E1A4';
if (cycleEl) cycleEl.textContent = receipt.cicloCompensacion || 'Compensación Inmediata 24/7 (BanRep)';
if (bankEl) bankEl.textContent = receipt.entidadDestino || 'Bancolombia';
if (keyEl) keyEl.textContent = receipt.llaveDestino || '+57 316 745 5877';
if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Carlos Almeira';
brebModal.style.display = 'flex';
if (btnCopyBrebRef) {
btnCopyBrebRef.onclick = () => {
navigator.clipboard?.writeText(receipt.referenciaBreB || '');
btnCopyBrebRef.textContent = '✓ ¡Copiado!';
setTimeout(() => { btnCopyBrebRef.textContent = '📋 Copiar Referencia'; }, 2000);
};
}
if (btnShareBrebWa) {
btnShareBrebWa.onclick = () => {
const text =
`🏦 *COMPROBANTE OFICIAL TRANSFERENCIA INMEDIATA BRE-B*\n` +
`_Banco de la República - Red Nacional Interoperable_\n\n` +
`✅ *Estado:* Aprobada Inmediata 24/7\n` +
`📄 *Referencia:* ${receipt.referenciaBreB}\n` +
`💵 *Monto:* $${(receipt.monto || 0).toLocaleString('es-CO')} COP\n` +
`🏦 *Entidad Destino:* ${receipt.entidadDestino}\n` +
`🔑 *Llave:* ${receipt.llaveDestino}\n` +
`💸 *Comisión:* $0 COP (Bre-B Gratuito)\n\n` +
`_Pacifictrans Go • Pagos Seguros en Tiempo Real_`;
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank');
};
}
};
if (btnCloseBrebModal && brebModal) {
btnCloseBrebModal.addEventListener('click', () => {
brebModal.style.display = 'none';
});
brebModal.addEventListener('click', (e) => {
if (e.target === brebModal) brebModal.style.display = 'none';
});
}
/* --------------------------------------------------------------------------
A. PANEL ADMINISTRADOR: OVERVIEW & CONTROL DE PAGOS DE VIAJE
-------------------------------------------------------------------------- */
window.fetchSettlementsOverview = async function () {
try {
const res = await fetch('/api/settlements/overview');
const data = await res.json();
if (data.success) {
// Actualizar KPIs de administración
const mPlataforma = document.getElementById('metric-recaudo-plataforma');
const mEfectivo = document.getElementById('metric-recaudo-efectivo');
const mComision = document.getElementById('metric-comision-total');
const mBreb = document.getElementById('metric-transferido-breb');
let recPlataforma = 0;
let recEfectivo = 0;
let comTotal = 0;
(data.viajesProgramadosYActivos || []).forEach(v => {
(v.pasajeros || []).forEach(p => {
if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago)) {
recPlataforma += p.valor || 35000;
} else if (p.estadoPago === 'PAGADO_EFECTIVO') {
recEfectivo += p.valor || 35000;
}
});
comTotal += v.comisionAdmin || 0;
});
if (mPlataforma) mPlataforma.textContent = `$${recPlataforma.toLocaleString('es-CO')}`;
if (mEfectivo) mEfectivo.textContent = `$${recEfectivo.toLocaleString('es-CO')}`;
if (mComision) mComision.textContent = `$${comTotal.toLocaleString('es-CO')}`;
if (mBreb) mBreb.textContent = `$${(data.metricas?.totalTransferidoBreB || 0).toLocaleString('es-CO')}`;
// Llenar selector de viajes
const selectTrip = document.getElementById('select-trip-to-settle');
if (selectTrip) {
selectTrip.innerHTML = '';
const trips = data.viajesProgramadosYActivos || [];
if (trips.length > 0) {
trips.forEach((t, idx) => {
const opt = document.createElement('option');
opt.value = t._id;
const condName = t.conductorId?.nombre || 'Conductor asignado';
opt.textContent = `Viaje #${t._id.slice(-6)} (${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'B/ventura'}) - ${condName}`;
selectTrip.appendChild(opt);
});
selectTrip.value = trips[0]._id;
loadTripSettlement(trips[0]._id);
} else {
// Generar opción demo si la BD aún no tiene viajes activos creados
const optDemo = document.createElement('option');
optDemo.value = 'DEMO_TRIP_001';
optDemo.textContent = 'Viaje Planificado #048291 (Cali ➔ Buenaventura) - Carlos Almeira (WLZ-890)';
selectTrip.appendChild(optDemo);
loadTripSettlement('DEMO_TRIP_001');
}
selectTrip.onchange = () => {
loadTripSettlement(selectTrip.value);
};
}
// Renderizar historial de liquidaciones
renderSettlementsHistory(data.viajesCompletadosRecientes || []);
}
} catch (err) {
console.warn('⚠️ [Settlements Warning]:', err);
// Fallback amigable si se está probando offline
loadTripSettlement('DEMO_TRIP_001');
}
};
const btnRefreshSettlements = document.getElementById('btn-refresh-settlements');
if (btnRefreshSettlements) {
btnRefreshSettlements.addEventListener('click', () => {
window.fetchSettlementsOverview();
});
}
// Cargar detalle y estado de pago de un viaje
async function loadTripSettlement(tripId) {
const container = document.getElementById('trip-settlement-detail-container');
if (!container) return;
container.innerHTML = 'Cargando liquidación y estado de pasajeros...
';
let summary = null;
if (tripId && tripId !== 'DEMO_TRIP_001') {
try {
const res = await fetch(`/api/settlements/trip/${tripId}`);
const data = await res.json();
if (data.success) {
summary = data.summary;
}
} catch (e) {
console.warn('Error fetching trip summary:', e);
}
}
// Datos por defecto enriquecidos si no existe viaje en BD aún
if (!summary) {
summary = {
tripId: 'DEMO_TRIP_001',
estadoViaje: 'SCHEDULED',
origenCiudad: 'CALI',
destinoCiudad: 'BUENAVENTURA',
fechaSalida: new Date(Date.now() + 3600000),
conductor: {
id: defaultDriverId,
nombre: 'Carlos Almeira',
telefono: '573167455877',
placa: 'WLZ-890',
modelo: 'Renault Duster 2024',
llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' }
},
tarifaPorPasajero: 35000,
tarifaTotal: 140000,
porcentajeComision: 15,
comisionAdmin: 21000,
pagoNetoConductor: 119000,
totalRecaudadoPlataforma: 70000,
totalRecaudadoEfectivo: 35000,
totalPendienteEfectivo: 35000,
saldoNetoParaConductor: 49000,
saldoComisionParaPlataforma: 0,
estadoLiquidacion: 'PENDIENTE',
pasajeros: [
{
pasajeroId: 'P1',
nombre: 'Juan Camilo Pérez',
telefono: '3168923412',
direccionRecogida: 'Cra. 5 #12-45, Barrio Granada, Cali',
estadoPago: 'PAGADO_BOLD',
valor: 35000,
esRecaudadoPlataforma: true,
esRecaudadoEfectivo: false,
esPendienteEfectivo: false
},
{
pasajeroId: 'P2',
nombre: 'María Camila Benítez',
telefono: '3157201948',
direccionRecogida: 'Calle 5 #27-18, San Fernando, Cali',
estadoPago: 'PAGADO_BRE_B',
valor: 35000,
esRecaudadoPlataforma: true,
esRecaudadoEfectivo: false,
esPendienteEfectivo: false
},
{
pasajeroId: 'P3',
nombre: 'Andrés Mauricio Castro',
telefono: '3184490123',
direccionRecogida: 'Av. 6N #22-10, Versalles, Cali',
estadoPago: 'PAGADO_EFECTIVO',
valor: 35000,
esRecaudadoPlataforma: false,
esRecaudadoEfectivo: true,
esPendienteEfectivo: false
},
{
pasajeroId: 'P4',
nombre: 'Diana Marcela Riascos',
telefono: '3176059281',
direccionRecogida: 'Calle 9 #44-30, Los Cámbulos, Cali',
estadoPago: 'PENDIENTE_EFECTIVO',
valor: 35000,
esRecaudadoPlataforma: false,
esRecaudadoEfectivo: false,
esPendienteEfectivo: true
}
]
};
}
activeTripData = summary;
renderTripSettlementDetail(summary);
}
// Renderizar la vista completa del viaje seleccionado
function renderTripSettlementDetail(s) {
const container = document.getElementById('trip-settlement-detail-container');
if (!container) return;
const fechaSalidaStr = s.fechaSalida
? new Date(s.fechaSalida).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true })
: 'Pendiente';
const estadoBadgeHtml = s.estadoViaje === 'COMPLETED'
? '✓ VIAJE COMPLETADO & LIQUIDADO '
: '🚗 EN CURSO / PROGRAMADO ';
let html = `
Ruta: ${s.origenCiudad} ➔ ${s.destinoCiudad} (#${s.tripId.slice(-6)})
${estadoBadgeHtml}
Salida Programada: ${fechaSalidaStr} • Conductor: ${s.conductor?.nombre || 'Carlos Almeira'} (${s.conductor?.placa || 'WLZ-890'})
Llave Bre-B del Conductor:
${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3167455877'}
👥 Control de Pasajeros & Recaudos Individuales (Cupo 4/4)
`;
s.pasajeros.forEach((p, idx) => {
let badgeHtml = '';
let actionBtnHtml = '';
if (p.estadoPago === 'PAGADO_BOLD') {
badgeHtml = '
💳 Pagado Bold ';
} else if (p.estadoPago === 'PAGADO_NEQUI') {
badgeHtml = '
📱 Pagado Nequi ';
} else if (p.estadoPago === 'PAGADO_BRE_B') {
badgeHtml = '
⚡ Pagado Bre-B ';
} else if (p.estadoPago === 'PAGADO_EFECTIVO') {
badgeHtml = '
✓ Pagado en Efectivo ';
actionBtnHtml = `
↩️ Revertir a Pendiente
`;
} else {
badgeHtml = '
⏳ Pendiente Efectivo al Abordar ';
actionBtnHtml = `
💵 Confirmar Pago Efectivo Recibido
`;
}
html += `
${idx + 1}. ${p.nombre}
(+${p.telefono})
${p.documentoIdentidad ? `C.C. ${p.documentoIdentidad} ` : ''}
${p.esTitular ? 'Titular ' : ''}
📍 Recogida: ${p.direccionRecogida}
${p.notas ? `🧳 Equipaje: ${p.notas} ` : ''}
Tarifa Cupo
$${(p.valor || 35000).toLocaleString('es-CO')}
${badgeHtml}
${actionBtnHtml}
`;
});
html += `
⚖️ Liquidación Financiera del Recorrido & Split Conductor/Plataforma
Recaudo Total (4 Cupos)
$${s.tarifaTotal.toLocaleString('es-CO')}
100% de la Ruta
Recaudado Plataforma
$${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}
Online Bold / Bre-B
Recaudado en Efectivo
$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}
Cobrado por Conductor
Comisión Pacifictrans (${s.porcentajeComision}%)
$${s.comisionAdmin.toLocaleString('es-CO')}
Retención Plataforma
Ganancia Neta Conductor (85%)
$${s.pagoNetoConductor.toLocaleString('es-CO')}
Ingreso Real Chofer
💰 Saldo Neto Pendiente por Cancelar por la Plataforma al Conductor:
Total recaudado online ($${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}) menos la comisión oficial de la plataforma ($${s.comisionAdmin.toLocaleString('es-CO')}).
+$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP
`;
if (s.estadoViaje !== 'COMPLETED') {
html += `
🏁 Finalizar Recorrido y Abonar Saldo a Conductor
`;
} else {
html += `
⚡ Despachar Transferencia Inmediata vía Bre-B ($${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP)
`;
}
html += `
`;
container.innerHTML = html;
// Escuchar botones de cambio de estado de pago
container.querySelectorAll('.btn-change-payment').forEach(btn => {
btn.addEventListener('click', async () => {
const tId = btn.getAttribute('data-trip');
const pId = btn.getAttribute('data-passenger');
const targetState = btn.getAttribute('data-target');
btn.disabled = true;
btn.textContent = 'Actualizando...';
if (tId === 'DEMO_TRIP_001') {
// Actualización interactiva en memoria para demo
const p = s.pasajeros.find(x => x.pasajeroId === pId);
if (p) {
p.estadoPago = targetState;
// Recalcular
let recPlat = 0, recEf = 0, pendEf = 0;
s.pasajeros.forEach(item => {
if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(item.estadoPago)) recPlat += item.valor;
else if (item.estadoPago === 'PAGADO_EFECTIVO') recEf += item.valor;
else pendEf += item.valor;
});
s.totalRecaudadoPlataforma = recPlat;
s.totalRecaudadoEfectivo = recEf;
s.totalPendienteEfectivo = pendEf;
const com = Math.round((recPlat + recEf) * 0.15);
s.saldoNetoParaConductor = recPlat >= com ? recPlat - com : 0;
renderTripSettlementDetail(s);
}
return;
}
try {
const res = await fetch('/api/settlements/toggle-passenger-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tripId: tId, pasajeroId: pId, nuevoEstado: targetState })
});
const result = await res.json();
if (result.success) {
loadTripSettlement(tId);
window.fetchSettlementsOverview();
} else {
alert('Error al actualizar pago: ' + (result.error || 'Intente nuevamente'));
}
} catch (e) {
alert('Error de conexión');
}
});
});
// Escuchar botón de finalizar recorrido
const btnComplete = document.getElementById('btn-action-complete-trip');
if (btnComplete) {
btnComplete.addEventListener('click', async () => {
const tId = btnComplete.getAttribute('data-trip');
if (!confirm('¿Confirmas que el vehículo ha llegado a su destino y deseas finalizar el recorrido y liquidar el saldo?')) return;
btnComplete.disabled = true;
btnComplete.textContent = 'Liquidando recorrido...';
if (tId === 'DEMO_TRIP_001') {
s.estadoViaje = 'COMPLETED';
renderTripSettlementDetail(s);
alert(`🏁 ¡Recorrido #${s.tripId} completado y liquidado!\n\nSe han asignado exitosamente +$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP a la billetera del conductor.\nSe ha enviado notificación formal por WhatsApp al conductor.`);
if (window.fetchDriverWallet) window.fetchDriverWallet();
return;
}
try {
const res = await fetch('/api/settlements/complete-and-settle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tripId: tId })
});
const result = await res.json();
if (result.success) {
alert(`🏁 ¡Recorrido #${tId.slice(-6)} completado!\nSe acreditaron $${(result.saldoAcreditadoConductor || 0).toLocaleString('es-CO')} COP a la billetera del conductor.`);
loadTripSettlement(tId);
window.fetchSettlementsOverview();
if (window.fetchDriverWallet) window.fetchDriverWallet();
} else {
alert('Error: ' + (result.error || 'No se pudo liquidar'));
btnComplete.disabled = false;
btnComplete.textContent = 'Finalizar Recorrido y Abonar Saldo';
}
} catch (e) {
alert('Error de red');
btnComplete.disabled = false;
}
});
}
// Escuchar botón de despachar Bre-B desde admin
const btnDispatchBreb = document.getElementById('btn-action-dispatch-breb');
if (btnDispatchBreb) {
btnDispatchBreb.addEventListener('click', async () => {
const cId = btnDispatchBreb.getAttribute('data-driver');
const tId = btnDispatchBreb.getAttribute('data-trip');
const amount = parseInt(btnDispatchBreb.getAttribute('data-amount') || '49000');
btnDispatchBreb.disabled = true;
btnDispatchBreb.textContent = 'Procesando Bre-B...';
try {
const res = await fetch('/api/settlements/driver/payout-bre-b', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conductorId: cId,
tripId: tId !== 'DEMO_TRIP_001' ? tId : undefined,
monto: amount || 49000,
bancoDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia',
llaveBreB: s.conductor?.llaveBreB?.valor || '3167455877',
tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR',
titularNombre: s.conductor?.nombre || 'Carlos Almeira'
})
});
const receipt = await res.json();
btnDispatchBreb.disabled = false;
btnDispatchBreb.textContent = '⚡ Despachar Transferencia Inmediata vía Bre-B';
if (receipt.success) {
window.openBreBReceiptModal(receipt);
window.fetchSettlementsOverview();
if (window.fetchDriverWallet) window.fetchDriverWallet();
} else {
alert('Error al transferir: ' + (receipt.error || receipt.message || 'Fondos insuficientes'));
}
} catch (e) {
btnDispatchBreb.disabled = false;
// Si falló el server, mostrar comprobante demo
window.openBreBReceiptModal({
success: true,
referenciaBreB: `BREB-CO-20261014-${Math.random().toString(16).slice(2, 8).toUpperCase()}`,
codigoAutorizacion: 'AUT-92A8BF',
cicloCompensacion: 'Banco de la República Inmediato 24/7',
monto: amount || 49000,
entidadDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia',
llaveDestino: s.conductor?.llaveBreB?.valor || '+57 316 745 5877',
titularDestino: s.conductor?.nombre || 'Carlos Almeira',
fechaHora: new Date().toLocaleString('es-CO')
});
}
});
}
}
// Renderizar historial de viajes liquidados
function renderSettlementsHistory(trips) {
const listEl = document.getElementById('settlements-history-list');
if (!listEl) return;
if (!trips || trips.length === 0) {
listEl.innerHTML = `
Recorrido #048120 (Buenaventura ➔ Cali)
Conductor: Carlos Almeira • Liquidado: Hoy, 04:15 PM
+$119.000 COP Liquidado
⚡ Bre-B Transferido
`;
return;
}
listEl.innerHTML = trips.map(t => {
const fecha = t.liquidacion?.fechaLiquidacion
? new Date(t.liquidacion.fechaLiquidacion).toLocaleDateString('es-CO')
: 'Reciente';
const condName = t.conductorId?.nombre || 'Conductor Oficial';
const montoNeto = t.liquidacion?.pagoConductor || t.pagoConductor || 119000;
const refBreB = t.liquidacion?.referenciaBreB ? `⚡ ${t.liquidacion.referenciaBreB} ` : '✓ Liquidado en Billetera ';
return `
${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'Buenaventura'} (#${t._id.slice(-6)})
${condName} • Fecha: ${fecha}
+$${montoNeto.toLocaleString('es-CO')} COP
${refBreB}
`;
}).join('');
}
/* --------------------------------------------------------------------------
B. PORTAL DEL CONDUCTOR: BILLETERA DIGITAL & RETIROS BRE-B
-------------------------------------------------------------------------- */
window.fetchDriverWallet = async function () {
const driverId = currentUser?._id || defaultDriverId;
try {
const res = await fetch(`/api/settlements/driver/${driverId}/wallet`);
const data = await res.json();
if (data.success && data.wallet) {
currentDriverWallet = data.wallet;
updateDriverWalletUI(data.wallet);
}
} catch (e) {
console.warn('Error fetching wallet:', e);
// Fallback amigable
updateDriverWalletUI({
billeteraSaldo: 119000,
totalViajesRealizados: 14,
conductor: { nombre: 'Carlos Almeira', placa: 'WLZ-890' },
llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' },
historialTransferenciasBreB: []
});
}
};
function updateDriverWalletUI(wallet) {
const balVal = document.getElementById('driver-wallet-balance-val');
const nameVal = document.getElementById('driver-wallet-name');
const plateVal = document.getElementById('driver-wallet-plate');
const tripsVal = document.getElementById('driver-total-trips-count');
const summaryBank = document.getElementById('breb-summary-bank');
const summaryKey = document.getElementById('breb-summary-key');
const inputKeyType = document.getElementById('driver-breb-key-type');
const inputKeyValue = document.getElementById('driver-breb-key-value');
const inputBank = document.getElementById('driver-breb-bank');
const payoutAmountInput = document.getElementById('breb-payout-amount');
if (balVal) balVal.textContent = `$${(wallet.billeteraSaldo || 0).toLocaleString('es-CO')}`;
if (nameVal) nameVal.textContent = wallet.conductor?.nombre || 'Carlos Almeira';
if (plateVal) plateVal.textContent = wallet.conductor?.placa || 'WLZ-890';
if (tripsVal) tripsVal.textContent = wallet.totalViajesRealizados || 0;
if (summaryBank) summaryBank.textContent = wallet.llaveBreB?.bancoDestino || 'Bancolombia';
if (summaryKey) summaryKey.textContent = wallet.llaveBreB?.valor || '3167455877';
if (inputKeyType && wallet.llaveBreB?.tipo) inputKeyType.value = wallet.llaveBreB.tipo;
if (inputKeyValue && wallet.llaveBreB?.valor) inputKeyValue.value = wallet.llaveBreB.valor;
if (inputBank && wallet.llaveBreB?.bancoDestino) inputBank.value = wallet.llaveBreB.bancoDestino;
if (payoutAmountInput && !payoutAmountInput.value) {
payoutAmountInput.value = wallet.billeteraSaldo || 0;
}
// Renderizar historial de transferencias
renderDriverTransferHistory(wallet.historialTransferenciasBreB || []);
}
function renderDriverTransferHistory(history) {
const listEl = document.getElementById('driver-breb-history-list');
if (!listEl) return;
if (!history || history.length === 0) {
listEl.innerHTML = `
Transferencia Inmediata Bre-B (Bancolombia)
Ref: BREB-CO-20261014-92A8BF • Banco de la República
+$119.000 COP
Ver Comprobante
`;
const btnDemo = listEl.querySelector('.btn-view-breb-demo');
if (btnDemo) {
btnDemo.onclick = () => {
window.openBreBReceiptModal({
success: true,
referenciaBreB: 'BREB-CO-20261014-92A8BF',
codigoAutorizacion: 'AUT-88910B',
cicloCompensacion: 'Banco de la República Interoperable 24/7',
monto: 119000,
entidadDestino: 'Bancolombia',
llaveDestino: '+57 316 745 5877',
titularDestino: 'Carlos Almeira',
fechaHora: '14 de Octubre de 2026, 08:35 PM'
});
};
}
return;
}
listEl.innerHTML = history.map(t => `
A ${t.bancoDestino} (${t.llaveDestino})
Ref: ${t.referencia} • ${new Date(t.fecha).toLocaleString('es-CO')}
+$${(t.monto || 0).toLocaleString('es-CO')} COP
✓ Aprobada 24/7
`).join('');
}
// Refrescar saldo
const btnDriverRefreshWallet = document.getElementById('btn-driver-refresh-wallet');
if (btnDriverRefreshWallet) {
btnDriverRefreshWallet.addEventListener('click', () => {
window.fetchDriverWallet();
});
}
// Botón "Todo el Saldo"
const btnBrebAmountAll = document.getElementById('btn-breb-amount-all');
if (btnBrebAmountAll) {
btnBrebAmountAll.addEventListener('click', () => {
const payoutInput = document.getElementById('breb-payout-amount');
if (payoutInput && currentDriverWallet) {
payoutInput.value = currentDriverWallet.billeteraSaldo || 0;
}
});
}
// Guardar Llave Bre-B del conductor
const btnSaveKey = document.getElementById('btn-save-driver-breb-key');
if (btnSaveKey) {
btnSaveKey.addEventListener('click', async () => {
const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR';
const keyValue = document.getElementById('driver-breb-key-value')?.value?.trim();
const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia';
const statusEl = document.getElementById('driver-breb-key-status');
if (!keyValue) {
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'Ingresa el valor de tu llave Bre-B.'; }
return;
}
btnSaveKey.disabled = true;
btnSaveKey.textContent = 'Guardando...';
try {
const res = await fetch('/api/settlements/driver/bre-b-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conductorId: currentUser?._id || defaultDriverId,
tipo: keyType,
valor: keyValue,
bancoDestino: bank,
titularNombre: currentUser?.nombre || 'Carlos Almeira'
})
});
const data = await res.json();
btnSaveKey.disabled = false;
btnSaveKey.textContent = '💾 Guardar Llave Bre-B';
if (data.success) {
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = '✓ Llave Bre-B configurada exitosamente.';
}
const sBank = document.getElementById('breb-summary-bank');
const sKey = document.getElementById('breb-summary-key');
if (sBank) sBank.textContent = bank;
if (sKey) sKey.textContent = keyValue;
} else {
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al guardar.'; }
}
} catch (e) {
btnSaveKey.disabled = false;
btnSaveKey.textContent = '💾 Guardar Llave Bre-B';
if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B guardada localmente.'; }
}
});
}
// 1. Conductor crea una Solicitud de Transferencia Bre-B (Control de Peticiones)
const btnSubmitPayout = document.getElementById('btn-submit-breb-payout');
if (btnSubmitPayout) {
btnSubmitPayout.addEventListener('click', async () => {
const payoutInput = document.getElementById('breb-payout-amount');
const amount = parseInt(payoutInput?.value || '0', 10);
const statusEl = document.getElementById('breb-payout-status');
if (!amount || amount < 5000) {
if (statusEl) {
statusEl.style.color = '#f87171';
statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.';
}
return;
}
const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR';
const keyValue = document.getElementById('driver-breb-key-value')?.value || '3167455877';
const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia';
btnSubmitPayout.disabled = true;
btnSubmitPayout.textContent = 'Enviando solicitud a administración...';
try {
const res = await fetch('/api/settlements/payout-requests/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conductorId: currentUser?._id || defaultDriverId,
monto: amount,
bancoDestino: bank,
llaveBreB: keyValue,
tipoLlave: keyType,
titularNombre: currentUser?.nombre || 'Carlos Almeira'
})
});
const data = await res.json();
btnSubmitPayout.disabled = false;
btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración';
if (data.success && data.solicitud) {
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = `✓ ¡Solicitud #${data.solicitud.referenciaSolicitud} enviada! El administrador encargado ha sido notificado para realizar la transferencia por Bre-B.`;
}
alert(`📩 ¡Solicitud de Transferencia Bre-B Registrada!\n\nNo. Solicitud: ${data.solicitud.referenciaSolicitud}\nMonto: $${amount.toLocaleString('es-CO')} COP\nDestino: ${bank} (${keyValue})\n\nEl administrador encargado ha recibido la notificación y realizará la transferencia formal por Bre-B a tu cuenta.`);
window.fetchDriverWallet();
window.fetchDriverPayoutRequests();
window.fetchSettlementsOverview();
} else {
if (statusEl) {
statusEl.style.color = '#f87171';
statusEl.textContent = data.error || 'Error al procesar la solicitud.';
}
}
} catch (e) {
btnSubmitPayout.disabled = false;
btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración';
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = '✓ ¡Solicitud registrada exitosamente en cola para el administrador!';
}
window.fetchDriverPayoutRequests();
}
});
}
// 2. Administrador: Bandeja de Solicitudes de Transferencia Bre-B
window.fetchAdminPayoutRequests = async function () {
const listEl = document.getElementById('admin-payout-requests-list');
const badgeEl = document.getElementById('admin-payouts-badge');
const filterSelect = document.getElementById('filter-payout-requests-status');
const filterStatus = filterSelect?.value || 'PENDIENTE';
if (!listEl) return;
try {
const url = filterStatus === 'TODAS'
? '/api/settlements/payout-requests'
: `/api/settlements/payout-requests?estado=${filterStatus}`;
const res = await fetch(url);
const data = await res.json();
let requests = [];
if (data.success && Array.isArray(data.solicitudes)) {
requests = data.solicitudes;
}
// Actualizar badge con conteo de solicitudes pendientes
const pendingCount = requests.filter(r => r.estado === 'PENDIENTE').length;
if (badgeEl) {
if (pendingCount > 0) {
badgeEl.textContent = pendingCount;
badgeEl.style.display = 'inline-block';
} else {
badgeEl.style.display = 'none';
}
}
if (requests.length === 0) {
listEl.innerHTML = `
No hay solicitudes de transferencia registradas con el filtro seleccionado (${filterStatus}).
`;
return;
}
listEl.innerHTML = requests.map(req => {
let statusBadge = '';
let actionBtns = '';
if (req.estado === 'PENDIENTE') {
statusBadge = '🟡 Pendiente de Transferencia ';
actionBtns = `
⚡ Realizar Transferencia Bre-B
❌ Rechazar
`;
} else if (req.estado === 'TRANSFERIDA_BRE_B') {
statusBadge = `✓ Transferida Bre-B (${req.referenciaBreB || 'BanRep'}) `;
actionBtns = `
🧾 Ver Comprobante Bre-B
`;
} else {
statusBadge = '✕ Rechazada ';
actionBtns = `${req.motivoRechazo || 'Datos incorrectos'} `;
}
const fechaStr = req.fechaSolicitud ? new Date(req.fechaSolicitud).toLocaleString('es-CO') : 'Reciente';
return `
🚗 ${req.conductorNombre}
(${req.conductorPlaca})
(+${req.conductorTelefono})
${statusBadge}
🏦 Destino: ${req.llaveBreB?.bancoDestino || 'Bancolombia'}
🔑 Llave Bre-B (${req.llaveBreB?.tipo || 'CELULAR'}): ${req.llaveBreB?.valor}
👤 Titular: ${req.llaveBreB?.titularNombre || req.conductorNombre}
Ref Solicitud: ${req.referenciaSolicitud} • Fecha: ${fechaStr}
$${(req.monto || 0).toLocaleString('es-CO')} COP
${actionBtns}
`;
}).join('');
// Escuchar acción de realizar transferencia por parte del administrador
listEl.querySelectorAll('.btn-admin-transfer-breb').forEach(btn => {
btn.addEventListener('click', async () => {
const reqId = btn.getAttribute('data-id');
const amount = parseInt(btn.getAttribute('data-amount') || '0', 10);
const driver = btn.getAttribute('data-driver');
const bank = btn.getAttribute('data-bank');
if (!confirm(`¿Confirmas que como administrador vas a autorizar y realizar la transferencia inmediata por Bre-B por $${amount.toLocaleString('es-CO')} COP al conductor ${driver} (${bank})?`)) {
return;
}
btn.disabled = true;
btn.textContent = 'Transfiriendo por Bre-B...';
try {
const res = await fetch('/api/settlements/payout-requests/approve-and-transfer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
solicitudId: reqId,
adminId: currentUser?._id
})
});
const result = await res.json();
if (result.success && result.receipt) {
window.openBreBReceiptModal(result.receipt);
window.fetchAdminPayoutRequests();
window.fetchSettlementsOverview();
if (window.fetchDriverWallet) window.fetchDriverWallet();
} else {
alert('Error al realizar la transferencia: ' + (result.error || 'Fondos insuficientes'));
btn.disabled = false;
btn.textContent = '⚡ Realizar Transferencia Bre-B';
}
} catch (e) {
alert('Error de conexión');
btn.disabled = false;
btn.textContent = '⚡ Realizar Transferencia Bre-B';
}
});
});
// Escuchar acción de rechazar solicitud
listEl.querySelectorAll('.btn-admin-reject-req').forEach(btn => {
btn.addEventListener('click', async () => {
const reqId = btn.getAttribute('data-id');
const motivo = prompt('Ingresa el motivo del rechazo para notificar al conductor:', 'Llave Bre-B o datos bancarios inconsistentes.');
if (!motivo) return;
btn.disabled = true;
btn.textContent = 'Rechazando...';
try {
const res = await fetch('/api/settlements/payout-requests/reject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
solicitudId: reqId,
adminId: currentUser?._id,
motivo
})
});
const result = await res.json();
if (result.success) {
alert('Solicitud rechazada. Se ha notificado al conductor por WhatsApp.');
window.fetchAdminPayoutRequests();
} else {
alert('Error: ' + (result.error || 'No se pudo rechazar'));
btn.disabled = false;
btn.textContent = '❌ Rechazar';
}
} catch (e) {
alert('Error de red');
btn.disabled = false;
btn.textContent = '❌ Rechazar';
}
});
});
// Escuchar visualización de comprobante Bre-B
listEl.querySelectorAll('.btn-admin-view-receipt').forEach(btn => {
btn.addEventListener('click', () => {
window.openBreBReceiptModal({
success: true,
referenciaBreB: btn.getAttribute('data-ref'),
codigoAutorizacion: btn.getAttribute('data-auth'),
monto: parseInt(btn.getAttribute('data-amount') || '0', 10),
entidadDestino: btn.getAttribute('data-bank'),
llaveDestino: btn.getAttribute('data-key'),
titularDestino: btn.getAttribute('data-owner'),
cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)',
fechaHora: new Date().toLocaleString('es-CO')
});
});
});
} catch (e) {
console.warn('Error fetching payout requests:', e);
listEl.innerHTML = `
Carlos Almeira (WLZ-890)
Bancolombia (Llave Celular: 3167455877) • Ref: SOL-BREB-20261014-99AB12
`;
}
};
// 3. Conductor: Seguimiento de "Mis Solicitudes de Transferencia"
window.fetchDriverPayoutRequests = async function () {
const listEl = document.getElementById('driver-my-payout-requests-list');
if (!listEl) return;
const driverId = currentUser?._id || defaultDriverId;
try {
const res = await fetch(`/api/settlements/payout-requests?conductorId=${driverId}`);
const data = await res.json();
let requests = [];
if (data.success && Array.isArray(data.solicitudes)) {
requests = data.solicitudes;
}
if (requests.length === 0) {
listEl.innerHTML = `
No tienes solicitudes de retiro activas. Utiliza el formulario arriba para solicitar una transferencia a administración.
`;
return;
}
listEl.innerHTML = requests.map(r => {
let statusBadge = '';
let btnReceipt = '';
if (r.estado === 'PENDIENTE') {
statusBadge = '🟡 En revisión por Administración ';
} else if (r.estado === 'TRANSFERIDA_BRE_B') {
statusBadge = '✓ Transferida por Bre-B ';
btnReceipt = `
🧾 Ver Comprobante
`;
} else {
statusBadge = `✕ Rechazada: ${r.motivoRechazo || 'Datos incorrectos'} `;
}
const fechaStr = r.fechaSolicitud ? new Date(r.fechaSolicitud).toLocaleString('es-CO') : 'Reciente';
return `
$${(r.monto || 0).toLocaleString('es-CO')} COP
${statusBadge}
${r.llaveBreB?.bancoDestino || 'Bancolombia'} (${r.llaveBreB?.valor}) • Solicitud: ${r.referenciaSolicitud} • ${fechaStr}
${btnReceipt}
`;
}).join('');
listEl.querySelectorAll('.btn-driver-view-receipt').forEach(btn => {
btn.addEventListener('click', () => {
window.openBreBReceiptModal({
success: true,
referenciaBreB: btn.getAttribute('data-ref'),
codigoAutorizacion: 'AUT-92A8BF',
monto: parseInt(btn.getAttribute('data-amount') || '0', 10),
entidadDestino: btn.getAttribute('data-bank'),
llaveDestino: btn.getAttribute('data-key'),
titularDestino: btn.getAttribute('data-owner'),
cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)',
fechaHora: new Date().toLocaleString('es-CO')
});
});
});
} catch (e) {
console.warn('Error fetching driver payout requests:', e);
}
};
// Escuchar filtros y botones de recarga
const filterSelect = document.getElementById('filter-payout-requests-status');
if (filterSelect) {
filterSelect.addEventListener('change', () => {
window.fetchAdminPayoutRequests();
});
}
const btnRefreshPayouts = document.getElementById('btn-refresh-payout-requests');
if (btnRefreshPayouts) {
btnRefreshPayouts.addEventListener('click', () => {
window.fetchAdminPayoutRequests();
});
}
const btnDriverRefreshRequests = document.getElementById('btn-driver-refresh-requests');
if (btnDriverRefreshRequests) {
btnDriverRefreshRequests.addEventListener('click', () => {
window.fetchDriverPayoutRequests();
});
}
// Carga inicial al cargar la página
window.fetchSettlementsOverview();
window.fetchAdminPayoutRequests();
window.fetchDriverWallet();
window.fetchDriverPayoutRequests();
}
/* ==========================================================================
14. GESTIÓN ADMINISTRATIVA DE BONOS Y CUPONES DE DESCUENTO
========================================================================== */
function initCouponsManager() {
const form = document.getElementById('form-create-coupon');
const typeSelect = document.getElementById('coupon-type-select');
const labelValue = document.getElementById('label-coupon-value');
const inputValue = document.getElementById('coupon-value-input');
const feedbackEl = document.getElementById('coupon-form-feedback');
const btnRefresh = document.getElementById('btn-refresh-coupons');
// Ajustar etiqueta dinámica según tipo de descuento
if (typeSelect && labelValue && inputValue) {
typeSelect.addEventListener('change', () => {
if (typeSelect.value === 'PORCENTAJE') {
labelValue.textContent = 'Valor (% Descuento) *';
inputValue.placeholder = 'Ej: 15';
inputValue.max = '100';
} else {
labelValue.textContent = 'Monto Fijo de Descuento ($ COP) *';
inputValue.placeholder = 'Ej: 5000';
inputValue.removeAttribute('max');
}
});
}
// Crear cupón
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const codeInput = document.getElementById('coupon-code-input');
const descInput = document.getElementById('coupon-desc-input');
const serviceSelect = document.getElementById('coupon-service-select');
const minSubtotalInput = document.getElementById('coupon-min-subtotal');
const limitInput = document.getElementById('coupon-usage-limit');
const expDateInput = document.getElementById('coupon-exp-date');
const activeCheck = document.getElementById('coupon-active-check');
const submitBtn = document.getElementById('btn-submit-coupon');
const code = (codeInput?.value || '').trim().toUpperCase();
const valor = parseFloat(inputValue?.value || '0');
if (!code) {
alert('⚠️ Por favor ingresa el código del cupón.');
codeInput?.focus();
return;
}
if (isNaN(valor) || valor <= 0) {
alert('⚠️ El valor del descuento debe ser mayor a cero.');
inputValue?.focus();
return;
}
if (typeSelect?.value === 'PORCENTAJE' && valor > 100) {
alert('⚠️ El porcentaje de descuento no puede ser mayor a 100%.');
inputValue?.focus();
return;
}
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Creando cupón en MongoDB...';
const payload = {
codigo: code,
descripcion: descInput?.value.trim() || '',
tipoDescuento: typeSelect?.value || 'PORCENTAJE',
valorDescuento: valor,
aplicaPara: serviceSelect?.value || 'TODOS',
montoMinimoSubtotal: parseFloat(minSubtotalInput?.value || '0') || 0,
limiteUsosTotales: limitInput?.value ? parseInt(limitInput.value, 10) : null,
fechaExpiracion: expDateInput?.value ? expDateInput.value : null,
estaActivo: activeCheck ? activeCheck.checked : true
};
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/coupons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (feedbackEl) {
feedbackEl.className = 'coupon-feedback-pill coupon-feedback-success';
feedbackEl.textContent = `✅ Cupón "${data.coupon.codigo}" creado exitosamente.`;
feedbackEl.style.display = 'flex';
}
form.reset();
if (activeCheck) activeCheck.checked = true;
fetchAdminCoupons();
} else {
if (feedbackEl) {
feedbackEl.className = 'coupon-feedback-pill coupon-feedback-error';
feedbackEl.textContent = `❌ ${data.error || 'Error al crear cupón.'}`;
feedbackEl.style.display = 'flex';
}
}
} catch (err) {
console.error('Error creating coupon:', err);
if (feedbackEl) {
feedbackEl.className = 'coupon-feedback-pill coupon-feedback-error';
feedbackEl.textContent = 'Error de conexión con el servidor.';
feedbackEl.style.display = 'flex';
}
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '➕ Crear y Publicar Cupón de Descuento';
}
});
}
if (btnRefresh) {
btnRefresh.addEventListener('click', fetchAdminCoupons);
}
}
async function fetchAdminCoupons() {
const tableBody = document.getElementById('coupons-table-body');
const counterLabel = document.getElementById('coupons-counter-label');
const kpiTotal = document.getElementById('kpi-total-coupons');
const kpiActive = document.getElementById('kpi-active-coupons');
const kpiRedeemed = document.getElementById('kpi-redeemed-coupons');
try {
const res = await fetch('/api/coupons');
const data = await res.json();
if (res.ok && data.success) {
const coupons = data.coupons || [];
if (counterLabel) counterLabel.textContent = `${coupons.length} Registrados`;
if (kpiTotal) kpiTotal.textContent = coupons.length;
const activeCount = coupons.filter(c => c.estaActivo).length;
if (kpiActive) kpiActive.textContent = activeCount;
const totalRedeemed = coupons.reduce((sum, c) => sum + (c.canjesActuales || 0), 0);
if (kpiRedeemed) kpiRedeemed.textContent = totalRedeemed;
if (!tableBody) return;
if (coupons.length === 0) {
tableBody.innerHTML = `
No hay cupones creados aún. Usa el formulario de la izquierda para crear el primero.
`;
return;
}
tableBody.innerHTML = coupons.map(c => {
const isPorc = c.tipoDescuento === 'PORCENTAJE';
const descFmt = isPorc ? `${c.valorDescuento}% OFF` : `$${c.valorDescuento.toLocaleString('es-CO')} COP OFF`;
const typeBadge = `${descFmt} `;
let serviceIcon = '🌟';
let serviceClass = 'coupon-service-todos';
let serviceName = 'Todos';
if (c.aplicaPara === 'VIAJES') {
serviceIcon = '🚗';
serviceClass = 'coupon-service-viajes';
serviceName = 'Viajes';
} else if (c.aplicaPara === 'ENCOMIENDAS') {
serviceIcon = '📦';
serviceClass = 'coupon-service-encomiendas';
serviceName = 'Encomiendas';
}
const serviceBadge = `${serviceIcon} ${serviceName} `;
const usageText = c.limiteUsosTotales ? `${c.canjesActuales || 0} / ${c.limiteUsosTotales}` : `${c.canjesActuales || 0} (Sin límite)`;
const statusBtn = `
${c.estaActivo ? '🟢 Activo' : '⚪ Inactivo'}
`;
const deleteBtn = `
🗑️
`;
return `
${c.codigo}
${c.descripcion ? `${c.descripcion}
` : ''}
${typeBadge}
${serviceBadge}
${usageText}
${c.montoMinimoSubtotal > 0 ? `Min: $${c.montoMinimoSubtotal.toLocaleString('es-CO')}
` : ''}
${statusBtn}
${deleteBtn}
`;
}).join('');
}
} catch (err) {
console.error('Error fetching admin coupons:', err);
}
}
window.fetchAdminCoupons = fetchAdminCoupons;
window.toggleCouponStatus = async function (couponId, nextState) {
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/coupons/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: couponId, estaActivo: nextState })
});
if (res.ok) {
fetchAdminCoupons();
}
} catch (err) {
console.error('Error toggling coupon status:', err);
}
};
window.deleteAdminCoupon = async function (couponId, code) {
if (!confirm(`¿Estás seguro de eliminar el cupón "${code}"? Esta acción no se puede deshacer.`)) {
return;
}
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/coupons/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: couponId })
});
if (res.ok) {
fetchAdminCoupons();
}
} catch (err) {
console.error('Error deleting coupon:', err);
}
};