// Pacific Trans 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 Pacific Trans 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();
initUserProfileManager();
initAdministratorsManager();
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) {
const roleBadge = currentUser.esSuperAdmin ? '👑 Superadministrador' : (currentUser.rol === 'ADMIN' ? '🛡️ Administrador' : '🎧 Operador');
adminNameEl.textContent = `${currentUser.nombre} (${roleBadge})`;
}
// Visibilidad de subpestaña de gestión de administradores (solo ADMIN / Superadmin)
document.querySelectorAll('#btn-admin-subtab-administradores').forEach(b => {
b.style.display = (currentUser.rol === 'ADMIN' || currentUser.esSuperAdmin) ? 'inline-flex' : 'none';
});
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 250);
if (window.fetchSettlementsOverview) window.fetchSettlementsOverview();
if (window.fetchAdminPayoutRequests) window.fetchAdminPayoutRequests();
if (window.fetchAdministrators) window.fetchAdministrators();
} 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-pacifictrans') || 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');
localStorage.removeItem('pt_token_expires_at');
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');
const viewAdministradores = document.getElementById('admin-view-administradores');
function setAdminSubTab(targetViewId) {
const allViews = [
{ id: 'radar', el: viewRadar },
{ id: 'tarifas', el: viewTarifas },
{ id: 'liquidaciones', el: viewLiquidaciones },
{ id: 'cupones', el: viewCupones },
{ id: 'administradores', el: viewAdministradores }
];
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();
}
if (targetViewId === 'administradores' && window.fetchAdministrators) {
window.fetchAdministrators();
}
}
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')));
document.querySelectorAll('#btn-admin-subtab-administradores').forEach(b => b.addEventListener('click', () => setAdminSubTab('administradores')));
// 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 (PACIFIC TRANS GO CON OTP WHATSAPP + EMAIL)
========================================================================== */
function initAuthSystem() {
const modal = document.getElementById('modal-auth-pacifictrans') || 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 rawPhone = phoneInput?.value.trim() || '';
const email = emailInput?.value.trim() || '';
const name = nameInput?.value.trim() || '';
let cleanPhone = rawPhone.replace(/\D/g, '');
if (cleanPhone.startsWith('57') && cleanPhone.length === 12) {
// Prefijo 57 incluido
} else if (cleanPhone.length === 10) {
cleanPhone = '57' + cleanPhone;
}
if (!cleanPhone || cleanPhone.length < 10) {
showError(reqError, 'Ingresa un número de celular válido (Ej: 300 123 4567).');
return;
}
currentIdentifier = cleanPhone;
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: cleanPhone, 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 || `+${cleanPhone}`;
// 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) {
const expiresAt = data.expiresAt || (Date.now() + 24 * 60 * 60 * 1000);
localStorage.setItem('pt_token', data.token);
localStorage.setItem('pt_token_expires_at', String(expiresAt));
currentUser = data.user;
updateUserUI(currentUser);
modal.classList.remove('active');
alert(`¡Bienvenido a Pacific Trans 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'));
});
}
// Cerrar sesión
if (btnLogout) {
btnLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
currentUser = null;
updateUserUI(null);
window.location.hash = '#reservas';
window.showView('public');
});
}
// Validar sesión persistida (vigencia de 24 horas)
checkPersistedSession();
}
async function checkPersistedSession() {
const token = localStorage.getItem('pt_token');
const expiresAt = localStorage.getItem('pt_token_expires_at');
if (!token) return;
// Comprobar lapso de 24 horas en el navegador
if (expiresAt && Date.now() > Number(expiresAt)) {
console.log('⏰ [Sesión Pacific Trans GO]: El token de 24 horas ha expirado. Requiere autenticación OTP.');
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
currentUser = null;
updateUserUI(null);
return;
}
try {
const res = await fetch(`/api/auth/me?token=${encodeURIComponent(token)}`);
if (res.ok) {
const data = await res.json();
if (data.user) {
currentUser = data.user;
if (data.user.tokenExpiresAt) {
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
}
updateUserUI(currentUser);
console.log(`✅ [Sesión Autenticada]: ${currentUser.nombre} (+${currentUser.telefono}) activo por 24 horas.`);
} else {
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
}
} else {
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
}
} 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) {
if (user.esSuperAdmin) {
userRoleBadge.className = 'badge-verified superadmin-badge';
userRoleBadge.innerHTML = '👑 SUPERADMIN';
} else if (user.rol === 'ADMIN') {
userRoleBadge.className = 'badge-verified admin-role-badge admin-role-admin';
userRoleBadge.innerHTML = '🛡️ ADMIN';
} else if (user.rol === 'OPERADOR') {
userRoleBadge.className = 'badge-verified admin-role-badge admin-role-operador';
userRoleBadge.innerHTML = '🎧 OPERADOR';
} else {
userRoleBadge.className = 'badge-verified';
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');
}
}
// Actualizar notificaciones para el usuario actual (o limpiar si cerró sesión)
if (typeof window.renderUserNotifications === 'function') {
window.renderUserNotifications();
}
}
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 • Pacific Trans 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 oficiales
const elGroupPacific = document.getElementById('cfg-group-pacifictrans');
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';
if (elGroupPacific) elGroupPacific.value = pacificGroup;
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%20Pacific%20Trans%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 badgePacific = document.getElementById('badge-pacific-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';
}
alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacific Trans GO ➔ ${data.pacificTrans?.groupName || 'Configurado'} (${data.pacificTrans?.resolvedJid || 'JID Asignado'})`);
} 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 Pacific Trans GO',
grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans 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 Oficial: "${payload.grupoWhatsAppPacificTrans}"`;
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 = 'BRE_B';
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;
}
// Actualizar texto del badge informativo según el método de pago
const gatewayBadgeText = document.getElementById('summary-gateway-badge-text');
if (gatewayBadgeText) {
if (selectedPaymentMethod === 'BRE_B') {
gatewayBadgeText.innerHTML = '⚡ Transferencia directa por QR Bre-B (Inmediato 24/7 sin comisión)';
} else if (selectedPaymentMethod === 'BOLD') {
gatewayBadgeText.innerHTML = '💳 Pago seguro en línea (Tarjetas Débito / Crédito, PSE y Nequi)';
} else if (selectedPaymentMethod === 'NEQUI') {
gatewayBadgeText.innerHTML = '📱 Transferencia directa a Nequi ';
} else {
gatewayBadgeText.innerHTML = '💵 Pago en efectivo directamente al conductor al abordar';
}
}
if (btnBookText) {
const methodText = selectedPaymentMethod === 'BRE_B'
? 'Pagar con QR Bre-B'
: (selectedPaymentMethod === 'BOLD'
? 'Pagar en Línea'
: (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar (Efectivo)'));
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-pacifictrans') || 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;
}
// Renderizar información o enlace de pago según el método seleccionado
if (alertBoldArea) {
if (payload.metodoPago === 'BRE_B') {
alertBoldArea.style.display = 'block';
const cleanWa = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, '');
alertBoldArea.innerHTML = `
⚡ Transferencia Inmediata mediante QR Bre-B
Total a transferir: $${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP por ${selectedSeats} cupo(s).
🏛️ Llave Bre-B Oficial (Celular): +${cleanWa}
📲 Interoperabilidad: Puedes transferir desde Bancolombia, Nequi, Daviplata, Dale o cualquier entidad financiera sin costo.
Referencia de Reserva: #${data.tripId?.slice(-6) || 'PT'}
📲 Enviar Comprobante Bre-B por WhatsApp
`;
} else 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' });
}
if (typeof window.addUserNotification === 'function' && currentUser) {
window.addUserNotification({
id: `trip-${data.tripId || Date.now()}`,
icon: '🚗',
title: `¡Reserva Confirmada! (${selectedSeats} Cupo${selectedSeats > 1 ? 's' : ''})`,
desc: `Viaje #${data.tripId?.slice(-6) || ''} en ruta ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}. Conductor y ruta asignados.`,
time: 'Hace un momento',
unread: true
}, currentUser);
}
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 Seguro...';
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 de pago seguro en línea para completar el pago.`);
} else {
alert(`✅ Guía generada correctamente: #${data.guiaSeguimiento}`);
}
if (typeof window.addUserNotification === 'function' && currentUser) {
window.addUserNotification({
id: `encomienda-${data.guiaSeguimiento || Date.now()}`,
icon: '📦',
title: `Guía Encomienda #${data.guiaSeguimiento || 'PT'} Generada`,
desc: `Envío ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'} registrado con tarifa $${(data.tarifaFinal || data.tarifaCalculada || 0).toLocaleString('es-CO')} COP. Seguimiento satelital activo.`,
time: 'Hace un momento',
unread: true
}, currentUser);
}
} 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 (FILTRADO POR USUARIO)
========================================================================== */
function getUserNotificationsStorageKey(user) {
if (!user) return null;
const phone = (user.telefono || user.id || user._id || '').toString().replace(/\D/g, '');
return phone ? `pt_notifications_${phone}` : `pt_notifications_user_${user._id || user.id}`;
}
function getInitialUserNotifications(user) {
const firstName = (user.nombre || 'Usuario').split(' ')[0];
const notifs = [
{
id: `welcome-${Date.now()}-1`,
icon: '🚗',
title: `¡Bienvenido a Pacific Trans GO, ${firstName}!`,
desc: `Tu cuenta está activa para reservar viajes puerta a puerta y enviar encomiendas entre Cali y Buenaventura.`,
time: 'Hace un momento',
unread: true
},
{
id: `sec-${Date.now()}-2`,
icon: '🛡️',
title: 'Identidad Verificada 2FA',
desc: `Tu número (+${user.telefono || ''}) está validado con OTP. Tus viajes y pagos están 100% protegidos.`,
time: 'Hace un momento',
unread: true
}
];
if (user.esSuperAdmin || user.rol === 'ADMIN') {
notifs.unshift({
id: `admin-${Date.now()}-0`,
icon: '👑',
title: 'Acceso Administrativo Habilitado',
desc: 'Tienes permisos autorizados para configurar tarifas, bonos de descuento y gestionar administradores.',
time: 'Hace un momento',
unread: true
});
} else if (user.rol === 'CONDUCTOR') {
notifs.unshift({
id: `driver-${Date.now()}-0`,
icon: '💳',
title: 'Billetera Conductor Bre-B Activa',
desc: 'Tu cuenta de chofer está habilitada para recibir transferencias de compensación inmediata 24/7.',
time: 'Hace un momento',
unread: true
});
}
return notifs;
}
function renderUserNotifications() {
const badge = document.getElementById('notif-unread-count');
const headerUnread = document.getElementById('notif-header-unread');
const listEl = document.getElementById('notif-items-list');
const waLink = document.getElementById('notif-wa-link');
if (waLink && currentTariffs.numeroAtencionWhatsApp) {
const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, '');
waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacific%20Trans%20GO`;
}
// Si no hay usuario autenticado, no mostrar notificaciones de otros usuarios
if (!currentUser) {
if (badge) {
badge.textContent = '0';
badge.style.display = 'none';
}
if (headerUnread) {
headerUnread.textContent = '0 nuevas';
headerUnread.style.display = 'none';
}
if (listEl) {
listEl.innerHTML = `
🔒
Inicia sesión con tu celular para ver tus notificaciones y mensajes pendientes.
Iniciar Sesión
`;
}
return;
}
// Usuario autenticado: cargar únicamente sus notificaciones
const key = getUserNotificationsStorageKey(currentUser);
let notifs = [];
try {
const raw = localStorage.getItem(key);
if (raw) {
notifs = JSON.parse(raw);
} else {
notifs = getInitialUserNotifications(currentUser);
localStorage.setItem(key, JSON.stringify(notifs));
}
} catch (e) {
notifs = getInitialUserNotifications(currentUser);
}
const unreadCount = notifs.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 (notifs.length === 0) {
listEl.innerHTML = 'No tienes notificaciones pendientes.
';
} else {
listEl.innerHTML = notifs.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 = notifs.find(n => n.id === id);
if (target && target.unread) {
target.unread = false;
localStorage.setItem(key, JSON.stringify(notifs));
renderUserNotifications();
}
});
});
}
}
}
window.renderUserNotifications = renderUserNotifications;
window.addUserNotification = function (notification, targetUser) {
const u = targetUser || currentUser;
if (!u) return;
const key = getUserNotificationsStorageKey(u);
if (!key) return;
let list = [];
try {
const raw = localStorage.getItem(key);
list = raw ? JSON.parse(raw) : getInitialUserNotifications(u);
} catch (e) {
list = [];
}
list.unshift({
id: notification.id || `notif-${Date.now()}`,
icon: notification.icon || '🚗',
title: notification.title,
desc: notification.desc,
time: notification.time || 'Hace un momento',
unread: true
});
if (list.length > 20) list = list.slice(0, 20);
localStorage.setItem(key, JSON.stringify(list));
if (currentUser && (currentUser.telefono === u.telefono || currentUser._id === u._id)) {
renderUserNotifications();
}
};
function initNotificationsSystem() {
const btnBell = document.getElementById('btn-toggle-notifs');
const dropdown = document.getElementById('notif-dropdown-panel');
const btnMarkAll = document.getElementById('btn-mark-all-read');
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', () => {
if (!currentUser) return;
const key = getUserNotificationsStorageKey(currentUser);
if (!key) return;
try {
let notifs = JSON.parse(localStorage.getItem(key) || '[]');
notifs.forEach(n => { n.unread = false; });
localStorage.setItem(key, JSON.stringify(notifs));
renderUserNotifications();
} catch (e) {}
});
}
renderUserNotifications();
}
/* ==========================================================================
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 310 987 6543';
if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Mario Fernando Gómez';
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` +
`_Pacific Trans 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) - Mario Fernando Gómez (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: 'Mario Fernando Gómez',
telefono: '573109876543',
placa: 'WLZ-890',
modelo: 'Renault Duster 2024',
llaveBreB: { tipo: 'CELULAR', valor: '3109876543', bancoDestino: 'Bancolombia', titularNombre: 'Mario Fernando Gómez' }
},
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 || 'Mario Fernando Gómez'} (${s.conductor?.placa || 'WLZ-890'})
Llave Bre-B del Conductor:
${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3109876543'}
👥 Control de Pasajeros & Recaudos Individuales (Cupo 4/4)
`;
s.pasajeros.forEach((p, idx) => {
let badgeHtml = '';
let actionBtnHtml = '';
if (p.estadoPago === 'PAGADO_BOLD') {
badgeHtml = '
💳 Pagado en Línea ';
} 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')}
En Línea / Bre-B
Recaudado en Efectivo
$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}
Cobrado por Conductor
Comisión Pacific Trans GO (${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 || '3109876543',
tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR',
titularNombre: s.conductor?.nombre || 'Mario Fernando Gómez'
})
});
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 310 987 6543',
titularDestino: s.conductor?.nombre || 'Mario Fernando Gómez',
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: Mario Fernando Gómez • 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: 'Mario Fernando Gómez', placa: 'WLZ-890' },
llaveBreB: { tipo: 'CELULAR', valor: '3109876543', bancoDestino: 'Bancolombia', titularNombre: 'Mario Fernando Gómez' },
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 || 'Mario Fernando Gómez';
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 || '3109876543';
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 310 987 6543',
titularDestino: 'Mario Fernando Gómez',
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 || 'Usuario Conductor'
})
});
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 || '3109876543';
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 || 'Usuario Conductor'
})
});
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 = `
Mario Fernando Gómez (WLZ-890)
Bancolombia (Llave Celular: 3109876543) • 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);
}
};
/* ==========================================================================
10. USER PROFILE MANAGER (INFORMACIÓN PERSONAL - CELULAR INMUTABLE)
========================================================================== */
function initUserProfileManager() {
const modal = document.getElementById('modal-user-profile');
const btnOpenNav = document.getElementById('btn-edit-profile-nav');
const btnOpenUser = document.getElementById('btn-open-user-profile');
const btnClose = document.getElementById('btn-close-profile-modal');
const btnCancel = document.getElementById('btn-cancel-profile');
const form = document.getElementById('form-update-profile');
const feedback = document.getElementById('profile-feedback');
const btnSave = document.getElementById('btn-save-profile');
const inputName = document.getElementById('profile-name');
const inputPhone = document.getElementById('profile-phone');
const inputDoc = document.getElementById('profile-doc');
const inputEmail = document.getElementById('profile-email');
const inputAddress = document.getElementById('profile-address');
const inputCity = document.getElementById('profile-city');
function openProfileModal() {
if (!currentUser) {
const authModal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone');
if (authModal) authModal.classList.add('active');
return;
}
if (inputName) inputName.value = currentUser.nombre || '';
if (inputPhone) inputPhone.value = `+${currentUser.telefono || ''}`;
if (inputDoc) inputDoc.value = currentUser.documentoIdentidad || '';
if (inputEmail) inputEmail.value = currentUser.email || '';
if (inputAddress) inputAddress.value = currentUser.direccionTexto || '';
if (inputCity) inputCity.value = currentUser.ciudad || 'Cali';
if (feedback) feedback.style.display = 'none';
if (modal) modal.classList.add('active');
}
function closeProfileModal() {
if (modal) modal.classList.remove('active');
}
if (btnOpenNav) btnOpenNav.addEventListener('click', openProfileModal);
if (btnOpenUser) btnOpenUser.addEventListener('click', openProfileModal);
if (btnClose) btnClose.addEventListener('click', closeProfileModal);
if (btnCancel) btnCancel.addEventListener('click', closeProfileModal);
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentUser) return;
const token = localStorage.getItem('pt_token') || '';
if (!token) {
alert('Sesión no encontrada. Por favor inicia sesión nuevamente.');
return;
}
if (btnSave) {
btnSave.disabled = true;
btnSave.textContent = '⏳ Guardando...';
}
if (feedback) feedback.style.display = 'none';
try {
const payload = {
nombre: inputName ? inputName.value.trim() : '',
documentoIdentidad: inputDoc ? inputDoc.value.trim() : '',
email: inputEmail ? inputEmail.value.trim() : '',
direccionTexto: inputAddress ? inputAddress.value.trim() : '',
ciudad: inputCity ? inputCity.value.trim() : 'Cali'
};
const res = await fetch('/api/users/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (feedback) {
feedback.innerHTML = `
✅ ${data.message || 'Información actualizada con éxito'}
`;
feedback.style.display = 'block';
}
if (data.user) {
currentUser = { ...currentUser, ...data.user };
updateUserUI(currentUser);
}
setTimeout(() => {
closeProfileModal();
}, 1200);
} else {
if (feedback) {
feedback.innerHTML = `
❌ ${data.error || 'No fue posible actualizar tu información'}
`;
feedback.style.display = 'block';
}
}
} catch (err) {
console.error('Error updating user profile:', err);
if (feedback) {
feedback.innerHTML = `
❌ Error de red al comunicar con el servidor
`;
feedback.style.display = 'block';
}
} finally {
if (btnSave) {
btnSave.disabled = false;
btnSave.textContent = '💾 Guardar Información Personal';
}
}
});
}
}
/* ==========================================================================
11. ADMINISTRATORS & ACCESS CONTROL MANAGER (EXCLUSIVO SUPERADMINISTRADOR)
========================================================================== */
function initAdministratorsManager() {
const formCreate = document.getElementById('form-create-admin');
const btnRefresh = document.getElementById('btn-refresh-administrators');
const feedbackCreate = document.getElementById('admin-create-feedback');
const btnSubmitCreate = document.getElementById('btn-submit-new-admin');
// Modal Editar Administrador
const modalEdit = document.getElementById('modal-edit-admin');
const btnCloseEdit = document.getElementById('btn-close-edit-admin-modal');
const btnCancelEdit = document.getElementById('btn-cancel-edit-admin');
const formEdit = document.getElementById('form-edit-admin');
const feedbackEdit = document.getElementById('edit-admin-feedback');
const btnSaveEdit = document.getElementById('btn-save-edit-admin');
const editId = document.getElementById('edit-admin-id');
const editName = document.getElementById('edit-admin-name');
const editPhone = document.getElementById('edit-admin-phone');
const editDoc = document.getElementById('edit-admin-doc');
const editEmail = document.getElementById('edit-admin-email');
const editRole = document.getElementById('edit-admin-role');
if (btnRefresh) {
btnRefresh.addEventListener('click', () => fetchAdministrators());
}
function closeEditModal() {
if (modalEdit) modalEdit.classList.remove('active');
}
if (btnCloseEdit) btnCloseEdit.addEventListener('click', closeEditModal);
if (btnCancelEdit) btnCancelEdit.addEventListener('click', closeEditModal);
// Crear Administrador
if (formCreate) {
formCreate.addEventListener('submit', async (e) => {
e.preventDefault();
const token = localStorage.getItem('pt_token') || '';
if (!token) return;
const nameInput = document.getElementById('new-admin-name');
const phoneInput = document.getElementById('new-admin-phone');
const docInput = document.getElementById('new-admin-doc');
const emailInput = document.getElementById('new-admin-email');
const roleSelect = document.getElementById('new-admin-role');
if (btnSubmitCreate) {
btnSubmitCreate.disabled = true;
btnSubmitCreate.textContent = '⏳ Registrando...';
}
if (feedbackCreate) feedbackCreate.style.display = 'none';
try {
const res = await fetch('/api/admin/administrators', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
nombre: nameInput ? nameInput.value.trim() : '',
telefono: phoneInput ? phoneInput.value.trim() : '',
documentoIdentidad: docInput ? docInput.value.trim() : '',
email: emailInput ? emailInput.value.trim() : '',
rol: roleSelect ? roleSelect.value : 'ADMIN'
})
});
const data = await res.json();
if (res.ok && data.success) {
if (feedbackCreate) {
feedbackCreate.innerHTML = `
✅ Administrador ${data.administrator.nombre} registrado con éxito.
`;
feedbackCreate.style.display = 'block';
}
formCreate.reset();
fetchAdministrators();
setTimeout(() => {
if (feedbackCreate) feedbackCreate.style.display = 'none';
}, 3500);
} else {
if (feedbackCreate) {
feedbackCreate.innerHTML = `
❌ ${data.error || 'No fue posible registrar al administrador'}
`;
feedbackCreate.style.display = 'block';
}
}
} catch (err) {
console.error('Error creating administrator:', err);
} finally {
if (btnSubmitCreate) {
btnSubmitCreate.disabled = false;
btnSubmitCreate.textContent = '➕ Registrar Administrador';
}
}
});
}
// Guardar edición de Administrador
if (formEdit) {
formEdit.addEventListener('submit', async (e) => {
e.preventDefault();
const token = localStorage.getItem('pt_token') || '';
if (!token) return;
const adminId = editId ? editId.value : '';
if (!adminId) return;
if (btnSaveEdit) {
btnSaveEdit.disabled = true;
btnSaveEdit.textContent = '⏳ Guardando...';
}
if (feedbackEdit) feedbackEdit.style.display = 'none';
try {
const payload = {
nombre: editName ? editName.value.trim() : '',
documentoIdentidad: editDoc ? editDoc.value.trim() : '',
email: editEmail ? editEmail.value.trim() : '',
rol: editRole ? editRole.value : 'ADMIN'
};
const res = await fetch(`/api/admin/administrators/${adminId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (feedbackEdit) {
feedbackEdit.innerHTML = `
✅ Cambios guardados correctamente.
`;
feedbackEdit.style.display = 'block';
}
fetchAdministrators();
setTimeout(() => {
closeEditModal();
}, 1200);
} else {
if (feedbackEdit) {
feedbackEdit.innerHTML = `
❌ ${data.error || 'Error al guardar los cambios'}
`;
feedbackEdit.style.display = 'block';
}
}
} catch (err) {
console.error('Error updating admin:', err);
} finally {
if (btnSaveEdit) {
btnSaveEdit.disabled = false;
btnSaveEdit.textContent = '💾 Guardar Cambios';
}
}
});
}
}
async function fetchAdministrators() {
const tbody = document.getElementById('table-administrators-body');
const counterLabel = document.getElementById('admins-counter-label');
const kpiAdmins = document.getElementById('kpi-admins-count');
const kpiOperators = document.getElementById('kpi-operators-count');
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/admin/administrators', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok || !data.success) {
if (tbody) {
tbody.innerHTML = `
❌ ${data.error || 'Error al consultar equipo de administradores'}
`;
}
return;
}
const admins = data.administrators || [];
// Actualizar contadores
let adminCount = 0;
let operatorCount = 0;
admins.forEach(a => {
if (a.rol === 'OPERADOR') operatorCount++;
else adminCount++;
});
if (counterLabel) counterLabel.textContent = `${admins.length} miembro(s) registrados`;
if (kpiAdmins) kpiAdmins.textContent = adminCount;
if (kpiOperators) kpiOperators.textContent = operatorCount;
if (!tbody) return;
if (admins.length === 0) {
tbody.innerHTML = `
No hay administradores registrados en la plataforma.
`;
return;
}
tbody.innerHTML = admins.map(admin => {
const isSuper = !!admin.esSuperAdmin;
const adminDataEncoded = encodeURIComponent(JSON.stringify(admin));
let roleBadge = '';
if (isSuper) {
roleBadge = `👑 SUPERADMIN `;
} else if (admin.rol === 'ADMIN') {
roleBadge = `🛡️ ADMIN `;
} else {
roleBadge = `🎧 OPERADOR `;
}
let statusHtml = '';
if (isSuper) {
statusHtml = `
🔒 Permanente
`;
} else {
const isActive = admin.isActive !== false;
statusHtml = `
${isActive ? '✅ Activo' : '⏸️ Inactivo'}
`;
}
let actionsHtml = '';
if (isSuper) {
actionsHtml = `🔒 Superadmin `;
} else {
actionsHtml = `
✏️ Editar
🗑️
`;
}
return `
${isSuper ? '👑 ' : '🛡️ '}
${admin.nombre}
${isSuper ? '👑 PRINCIPAL ' : ''}
${admin.documentoIdentidad ? `C.C. ${admin.documentoIdentidad} • ` : ''}${admin.email || 'Sin correo registrado'}
📲
+${admin.telefono}
2FA OTP Habilitado
${roleBadge}
${statusHtml}
${actionsHtml}
`;
}).join('');
} catch (err) {
console.error('Error fetching administrators:', err);
if (tbody) {
tbody.innerHTML = `
❌ Error de conexión al cargar equipo de administradores
`;
}
}
}
window.fetchAdministrators = fetchAdministrators;
window.openEditAdminModal = function (adminDataEncoded) {
try {
const admin = JSON.parse(decodeURIComponent(adminDataEncoded));
const modal = document.getElementById('modal-edit-admin');
const editId = document.getElementById('edit-admin-id');
const editName = document.getElementById('edit-admin-name');
const editPhone = document.getElementById('edit-admin-phone');
const editDoc = document.getElementById('edit-admin-doc');
const editEmail = document.getElementById('edit-admin-email');
const editRole = document.getElementById('edit-admin-role');
const editRoleGroup = document.getElementById('edit-admin-role-group');
const subtitle = document.getElementById('edit-admin-subtitle');
const feedback = document.getElementById('edit-admin-feedback');
if (editId) editId.value = admin.id || '';
if (editName) editName.value = admin.nombre || '';
if (editPhone) editPhone.value = `+${admin.telefono || ''}`;
if (editDoc) editDoc.value = admin.documentoIdentidad || '';
if (editEmail) editEmail.value = admin.email || '';
if (editRole) editRole.value = admin.rol || 'ADMIN';
if (admin.esSuperAdmin) {
if (editRoleGroup) editRoleGroup.style.display = 'none';
if (subtitle) subtitle.textContent = 'Actualizar datos de contacto del Superadministrador permanente';
} else {
if (editRoleGroup) editRoleGroup.style.display = 'block';
if (subtitle) subtitle.textContent = 'Actualizar permisos y datos de contacto del colaborador';
}
if (feedback) feedback.style.display = 'none';
if (modal) modal.classList.add('active');
} catch (e) {
console.error('Error parsing admin for edit:', e);
}
};
window.toggleAdminActive = async function (adminId, nextState, adminName) {
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch(`/api/admin/administrators/${adminId}/toggle-active`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: adminId, isActive: nextState })
});
const data = await res.json();
if (res.ok && data.success) {
fetchAdministrators();
} else {
alert(`Error al cambiar estado: ${data.error || 'No permitido'}`);
}
} catch (err) {
console.error('Error toggling admin active:', err);
alert('Error al comunicar con el servidor.');
}
};
window.deleteAdmin = async function (adminId, adminName) {
if (!confirm(`¿Estás seguro de eliminar a ${adminName || 'este administrador'} del equipo? Esta acción revoca inmediatamente sus permisos de acceso.`)) {
return;
}
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch(`/api/admin/administrators/${adminId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: adminId })
});
const data = await res.json();
if (res.ok && data.success) {
fetchAdministrators();
} else {
alert(`Error al eliminar administrador: ${data.error || 'No permitido'}`);
}
} catch (err) {
console.error('Error deleting administrator:', err);
alert('Error al comunicar con el servidor.');
}
};