22// Pacifictrans Go - Interactive Web Platform Logic let currentTariffs = { tarifaPasajeroFija: 35000, precioBasePaquete: 25000, precioKmPaquete: 1200, factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 }, seguroBasePaquete: 5000, porcentajeComisionAdmin: 15, costoPenalidadCancelacion: 15000, radioMaxPoolingMetros: 4000, numeroAtencionWhatsApp: '573167455877', nombreSoporte: 'Atención Pacifictrans Go' }; let currentUser = null; let leafletMap = null; let deferredPrompt = null; // Drivers Flota con coordenadas reales en el corredor Cali - Buenaventura const activeDrivers = [ { id: '1', nombre: 'Mario Fernando G.', vehiculo: 'Renault Duster (WLZ-890)', barrio: 'Granada (Comuna 2)', distCali: '1.2 km', lat: 3.4552, lng: -76.5350, estado: 'DISPONIBLE' }, { id: '2', nombre: 'Hernán Darío P.', vehiculo: 'Toyota Fortuner (VBF-312)', barrio: 'San Antonio (Comuna 3)', distCali: '2.4 km', lat: 3.4485, lng: -76.5415, estado: 'ASIGNADO' }, { id: '3', nombre: 'Mauricio V.', vehiculo: 'Chevrolet Tracker (HJK-456)', barrio: 'El Ingenio (Comuna 17)', distCali: '6.8 km', lat: 3.3850, lng: -76.5290, estado: 'DISPONIBLE' }, { id: '4', nombre: 'Javier Restrepo', vehiculo: 'Nissan Kicks (RTY-789)', barrio: 'Limonar (Comuna 19)', distCali: '5.1 km', lat: 3.6580, lng: -76.6900, estado: 'EN_RUTA' } ]; document.addEventListener('DOMContentLoaded', () => { initPWA(); initTabs(); initAuthSystem(); initNotificationsSystem(); initPassengerCalculator(); initFreightCalculator(); initLeafletRadarMap(); initReviewsAndStats(); initTariffsManager(); initCouponsManager(); initSettlementsAndBreBSystem(); 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-dripzone'); [btnGateAdminLogin, btnGateDriverLogin].forEach(b => { if (b && authModal) { b.addEventListener('click', () => authModal.classList.add('active')); } }); const btnAdminLogout = document.getElementById('btn-admin-logout'); if (btnAdminLogout) { btnAdminLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); currentUser = null; if (window.updateUserUI) window.updateUserUI(null); window.location.hash = '#reservas'; window.showView('public'); }); } // Compatibilidad genérica para pestañas window.activateTab = function (targetId) { tabBtns.forEach(b => { if (b.getAttribute('data-tab') === targetId) { b.classList.add('active'); } else { b.classList.remove('active'); } }); tabContents.forEach(c => { if (c.id === targetId) { c.classList.add('active'); } else { c.classList.remove('active'); } }); if (targetId === 'tab-admin' && leafletMap) { setTimeout(() => { leafletMap.invalidateSize(); }, 250); } }; tabBtns.forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-tab'); window.activateTab(targetId); }); }); // Sub-tabs de la sección administrativa const viewRadar = document.getElementById('admin-view-radar'); const viewTarifas = document.getElementById('admin-view-tarifas'); const viewLiquidaciones = document.getElementById('admin-view-liquidaciones'); const viewCupones = document.getElementById('admin-view-cupones'); 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 (DRIPZONE STYLE CON OTP WHATSAPP + EMAIL) ========================================================================== */ function initAuthSystem() { const modal = document.getElementById('modal-auth-dripzone'); const btnOpen = document.getElementById('btn-open-auth-modal'); const btnClose = document.getElementById('btn-close-auth-modal'); const btnLogout = document.getElementById('btn-logout'); const modeLoginBtn = document.getElementById('btn-auth-mode-login'); const modeRegisterBtn = document.getElementById('btn-auth-mode-register'); const groupName = document.getElementById('group-auth-name'); const authModalTitle = document.getElementById('auth-modal-title'); const step1 = document.getElementById('auth-step-1'); const step2 = document.getElementById('auth-step-2'); const formStep1 = document.getElementById('form-auth-request-otp'); const btnVerifyOtp = document.getElementById('btn-submit-verify-otp'); const btnResend = document.getElementById('btn-auth-resend-otp'); const reqError = document.getElementById('auth-request-error'); const verError = document.getElementById('auth-verify-error'); const targetLabel = document.getElementById('auth-sent-target'); let authMode = 'login'; // 'login' | 'register' let currentIdentifier = ''; let currentEmail = ''; // Abrir y cerrar modal if (btnOpen && modal) { btnOpen.addEventListener('click', () => { reqError.style.display = 'none'; verError.style.display = 'none'; step1.style.display = 'block'; step2.style.display = 'none'; modal.classList.add('active'); }); } if (btnClose && modal) { btnClose.addEventListener('click', () => modal.classList.remove('active')); modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('active'); }); } // Alternar entre Login y Registro if (modeLoginBtn && modeRegisterBtn) { modeLoginBtn.addEventListener('click', () => { authMode = 'login'; modeLoginBtn.classList.add('active'); modeRegisterBtn.classList.remove('active'); if (groupName) groupName.style.display = 'none'; if (authModalTitle) authModalTitle.textContent = 'Acceso Seguro con Verificación OTP'; }); modeRegisterBtn.addEventListener('click', () => { authMode = 'register'; modeRegisterBtn.classList.add('active'); modeLoginBtn.classList.remove('active'); if (groupName) groupName.style.display = 'block'; if (authModalTitle) authModalTitle.textContent = 'Registro de Usuario & Validación 2FA'; }); } // Paso 1: Enviar OTP por WhatsApp if (formStep1) { formStep1.addEventListener('submit', async (e) => { e.preventDefault(); const phoneInput = document.getElementById('auth-input-phone'); const emailInput = document.getElementById('auth-input-email'); const nameInput = document.getElementById('auth-input-name'); const btnSubmit = document.getElementById('btn-submit-send-otp'); const phone = phoneInput?.value.trim() || ''; const email = emailInput?.value.trim() || ''; const name = nameInput?.value.trim() || ''; if (!phone) { showError(reqError, 'Ingresa tu número de WhatsApp.'); return; } currentIdentifier = phone; currentEmail = email; btnSubmit.disabled = true; btnSubmit.innerHTML = '⏳ Enviando código de seguridad...'; reqError.style.display = 'none'; try { const res = await fetch('/api/auth/send-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: phone, email }) }); const data = await res.json(); if (res.ok && data.success) { step1.style.display = 'none'; step2.style.display = 'block'; if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${phone}`; // Si el servidor devolvió preview (entorno local sin SIM de Evolution), mostrar aviso if (data.codePreview) { console.log(`[Demo Code OTP]: ${data.codePreview}`); verError.style.display = 'block'; verError.style.color = '#38bdf8'; verError.innerHTML = `ℹ️ Código de prueba generado: ${data.codePreview}`; } focusFirstOtp(); } else { showError(reqError, data.error || 'No se pudo enviar el código OTP.'); } } catch (err) { showError(reqError, 'Error de conexión. Inténtalo de nuevo.'); } finally { btnSubmit.disabled = false; btnSubmit.innerHTML = '📲 Enviar Código de Verificación OTP'; } }); } // Cajas OTP con avance automático y soporte de pegado const otpInputs = [ document.getElementById('otp-1'), document.getElementById('otp-2'), document.getElementById('otp-3'), document.getElementById('otp-4'), document.getElementById('otp-5'), document.getElementById('otp-6') ]; otpInputs.forEach((input, index) => { if (!input) return; input.addEventListener('input', (e) => { const val = e.target.value; if (val.length === 1 && index < 5) { otpInputs[index + 1].focus(); } checkAutoSubmitOtp(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !input.value && index > 0) { otpInputs[index - 1].focus(); } }); input.addEventListener('paste', (e) => { e.preventDefault(); const pasteData = e.clipboardData.getData('text').trim(); if (/^\d{6}$/.test(pasteData)) { pasteData.split('').forEach((digit, i) => { if (otpInputs[i]) otpInputs[i].value = digit; }); checkAutoSubmitOtp(); } }); }); function focusFirstOtp() { otpInputs.forEach(i => { if (i) i.value = ''; }); if (otpInputs[0]) otpInputs[0].focus(); } function getEnteredOtp() { return otpInputs.map(i => i?.value || '').join(''); } function checkAutoSubmitOtp() { const code = getEnteredOtp(); if (code.length === 6) { submitVerifyOtp(code); } } // Paso 2: Verificar OTP if (btnVerifyOtp) { btnVerifyOtp.addEventListener('click', () => { const code = getEnteredOtp(); if (code.length < 6) { showError(verError, 'Ingresa los 6 dígitos del código.'); return; } submitVerifyOtp(code); }); } async function submitVerifyOtp(code) { if (!btnVerifyOtp) return; btnVerifyOtp.disabled = true; btnVerifyOtp.innerHTML = '⏳ Verificando código...'; verError.style.display = 'none'; const nameInput = document.getElementById('auth-input-name'); const name = nameInput?.value.trim() || ''; try { const res = await fetch('/api/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: currentIdentifier, code, nombre: name, email: currentEmail }) }); const data = await res.json(); if (res.ok && data.success) { localStorage.setItem('pt_token', data.token); currentUser = data.user; updateUserUI(currentUser); modal.classList.remove('active'); alert(`¡Bienvenido a Pacifictrans Go, ${currentUser.nombre}! Tu identidad ha sido verificada con éxito.`); } else { showError(verError, data.error || 'Código incorrecto. Verifica los 6 dígitos.'); } } catch (err) { showError(verError, 'Error al validar el código OTP.'); } finally { btnVerifyOtp.disabled = false; btnVerifyOtp.innerHTML = '✅ Validar y Acceder'; } } // Reenviar OTP if (btnResend) { btnResend.addEventListener('click', () => { formStep1.dispatchEvent(new Event('submit')); }); } // Selector Rápido de Roles para Auditoría y Pruebas document.querySelectorAll('.btn-role-quick-login').forEach(btn => { btn.addEventListener('click', async () => { const role = btn.getAttribute('data-role') || 'ADMIN'; // El ingreso como Administrador SIEMPRE exige verificación de Token/OTP por WhatsApp if (role === 'ADMIN' || role === 'OPERADOR') { const phoneInput = document.getElementById('auth-phone'); if (phoneInput) { phoneInput.value = '3167455877'; } authMode = 'login'; if (modeLoginBtn) modeLoginBtn.classList.add('active'); if (modeRegisterBtn) modeRegisterBtn.classList.remove('active'); if (groupName) groupName.style.display = 'none'; if (reqError) { reqError.textContent = '🔒 Acceso de Administrador: Por políticas de seguridad 2FA, se enviará el código OTP a tu WhatsApp personal (+57 316 745 5877).'; reqError.style.color = '#38bdf8'; reqError.style.display = 'block'; } // Enviar OTP vía WhatsApp formStep1.dispatchEvent(new Event('submit')); return; } btn.disabled = true; const originalText = btn.innerHTML; btn.innerHTML = '⏳ Accediendo...'; try { const res = await fetch('/api/auth/quick-role-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }) }); const data = await res.json(); if (data.success && data.user) { localStorage.setItem('pt_token', data.token); currentUser = data.user; updateUserUI(currentUser); if (modal) modal.classList.remove('active'); if (role === 'CONDUCTOR') { window.location.hash = '#conductor'; window.showView('driver'); } else { window.location.hash = '#reservas'; window.showView('public'); } } else { alert('Error de acceso rápido: ' + (data.error || 'Intenta de nuevo')); } } catch (e) { console.error('Quick role error:', e); alert('Error de conexión al servidor'); } finally { btn.disabled = false; btn.innerHTML = originalText; } }); }); // Cerrar sesión if (btnLogout) { btnLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); currentUser = null; updateUserUI(null); window.location.hash = '#reservas'; window.showView('public'); }); } // Validar sesión persistida checkPersistedSession(); } async function checkPersistedSession() { const token = localStorage.getItem('pt_token'); if (!token) return; try { const res = await fetch(`/api/auth/me?token=${token}`); if (res.ok) { const data = await res.json(); if (data.user) { currentUser = data.user; updateUserUI(currentUser); } } else { localStorage.removeItem('pt_token'); } } catch (err) { console.warn('⚠️ [Session Verify Error]:', err); } } function updateUserUI(user) { const btnAuthOpen = document.getElementById('btn-open-auth-modal'); const userBadge = document.getElementById('user-profile-badge'); const userNameEl = document.getElementById('user-display-name'); const userRoleBadge = document.getElementById('user-role-badge'); const navBtnAdmin = document.getElementById('nav-btn-open-admin'); const navBtnDriver = document.getElementById('nav-btn-open-driver'); const inputUserName = document.getElementById('user-name'); const inputUserPhone = document.getElementById('user-phone'); const inputSenderPhone = document.getElementById('sender-phone'); if (user) { if (btnAuthOpen) btnAuthOpen.style.display = 'none'; if (userBadge) userBadge.style.display = 'inline-flex'; if (userNameEl) userNameEl.textContent = user.nombre.split(' ')[0]; if (userRoleBadge) { 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'); } } } function showError(el, msg) { if (!el) return; el.textContent = msg; el.style.display = 'block'; } /* ========================================================================== 4. LEAFLET RADAR FLEET MAP (MAPA ESTÁNDAR CON TOOLTIPS) ========================================================================== */ function initLeafletRadarMap() { const mapContainer = document.getElementById('fleet-live-map'); if (!mapContainer || typeof L === 'undefined') return; // Centro entre Cali (3.45) y Buenaventura (3.88) const corridorCenter = [3.65, -76.78]; leafletMap = L.map('fleet-live-map', { center: corridorCenter, zoom: 10, zoomControl: true, scrollWheelZoom: false }); // Capa de Mapa Estándar OpenStreetMap L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '© OpenStreetMap contributors • Pacifictrans Go GPS' }).addTo(leafletMap); // Línea poligonal oficial de la Carretera Vía al Mar (Ruta 40) const corridorWaypoints = [ [3.4516, -76.5320], // Cali [3.4750, -76.5650], // Portada al Mar [3.5800, -76.6300], // Km 18 / Saladito [3.6580, -76.6900], // Dagua [3.7650, -76.7350], // Loboguerrero [3.7920, -76.8150], // Cisneros [3.8450, -76.9200], // Córdoba [3.8814, -77.0315] // Buenaventura ]; L.polyline(corridorWaypoints, { color: '#06b6d4', weight: 5, opacity: 0.85, dashArray: '8, 6' }).addTo(leafletMap); // Marcadores de Hitos Urbanos addCityMarker(leafletMap, [3.4516, -76.5320], '🟢 Cali (Km 0)'); addCityMarker(leafletMap, [3.6580, -76.6900], '🔵 Dagua (Km 45)'); addCityMarker(leafletMap, [3.7650, -76.7350], '🔵 Loboguerrero (Km 60)'); addCityMarker(leafletMap, [3.8814, -77.0315], '🟡 Buenaventura (Km 115)'); // Vehículos de la Flota en Tiempo Real activeDrivers.forEach((driver, index) => { const customIcon = L.divIcon({ className: 'vehicle-marker-icon', html: `
🚗
`, iconSize: [44, 44], iconAnchor: [22, 22] }); const marker = L.marker([driver.lat, driver.lng], { icon: customIcon }).addTo(leafletMap); // Tooltip enriquecido al pasar el cursor o pulsar const tooltipContent = `
#${index + 1} ${driver.nombre} ● ${driver.estado}
${driver.vehiculo}
🏠 Domicilio: ${driver.barrio}
📍 Cercanía a 1er Pasajero: ${driver.distCali}
`; 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}
`).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 `
${r.autorNombre}
${isPasajero ? '👤 Pasajero Verificado' : '🚗 Conductor Certificado'}
${starsString}
Ruta: ${r.ruta || 'Cali ➔ Buenaventura'} • Calificó a: ${r.destinatarioNombre}
"${r.comentario}"
${tagsHtml}
`; }).join(''); } /* ========================================================================== 6. CONFIGURACIÓN DE TARIFAS Y CANAL DE ATENCIÓN WHATSAPP ========================================================================== */ async function fetchTariffs() { try { const res = await fetch('/api/tariffs'); if (res.ok) { const data = await res.json(); if (data.config) { currentTariffs = { ...currentTariffs, ...data.config }; syncTariffsToUI(); } } } catch (err) { console.warn('⚠️ [Tarifas API]: Modo local/offline activo, utilizando tarifas por defecto.', err); syncTariffsToUI(); } } function syncTariffsToUI() { // 1. Formulario administrativo de tarifas y canales oficiales const elPasajero = document.getElementById('cfg-tarifa-pasajero'); const elBasePaquete = document.getElementById('cfg-base-paquete'); const elKmPaquete = document.getElementById('cfg-km-paquete'); const elSobre = document.getElementById('cfg-factor-sobre'); const elMediano = document.getElementById('cfg-factor-mediano'); const elGrande = document.getElementById('cfg-factor-grande'); const elSeguro = document.getElementById('cfg-seguro-base'); const elComision = document.getElementById('cfg-comision'); const elPenalidad = document.getElementById('cfg-penalidad'); const elRadio = document.getElementById('cfg-radio-pooling'); const elPhone = document.getElementById('cfg-admin-phone'); const elName = document.getElementById('cfg-admin-name'); // Canales de WhatsApp y separación de grupos por App const elGroupPacific = document.getElementById('cfg-group-pacifictrans'); const elGroupDripzone = document.getElementById('cfg-group-dripzone'); const adminWaGroupBadge = document.getElementById('admin-wa-group-badge'); if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija; if (elBasePaquete) elBasePaquete.value = currentTariffs.precioBasePaquete; if (elKmPaquete) elKmPaquete.value = currentTariffs.precioKmPaquete; if (elSobre && currentTariffs.factorPaquetes) elSobre.value = currentTariffs.factorPaquetes.sobre || 1.0; if (elMediano && currentTariffs.factorPaquetes) elMediano.value = currentTariffs.factorPaquetes.mediano || 1.6; if (elGrande && currentTariffs.factorPaquetes) elGrande.value = currentTariffs.factorPaquetes.grande || 2.2; if (elSeguro) elSeguro.value = currentTariffs.seguroBasePaquete; if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin; if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion; if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros; if (elPhone && currentTariffs.numeroAtencionWhatsApp) elPhone.value = currentTariffs.numeroAtencionWhatsApp; if (elName && currentTariffs.nombreSoporte) elName.value = currentTariffs.nombreSoporte; const pacificGroup = currentTariffs.grupoWhatsAppPacificTrans || 'Pacific Trans Notificaciones'; const dripGroup = currentTariffs.grupoWhatsAppDripzone || 'Dripzone Notificaciones'; if (elGroupPacific) elGroupPacific.value = pacificGroup; if (elGroupDripzone) elGroupDripzone.value = dripGroup; if (adminWaGroupBadge) adminWaGroupBadge.textContent = `👥 Grupo: ${pacificGroup}`; // 2. Sincronización del enlace dinámico de atención WhatsApp en el footer y navbar const phoneFormatted = formatPhoneNumber(currentTariffs.numeroAtencionWhatsApp || '573167455877'); const cleanPhone = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, ''); const footerPhoneEl = document.getElementById('footer-support-phone'); const footerLinkEl = document.getElementById('footer-support-link'); if (footerPhoneEl) footerPhoneEl.textContent = phoneFormatted; if (footerLinkEl) footerLinkEl.href = `https://wa.me/${cleanPhone}?text=Hola%20Pacifictrans%20Go,%20deseo%20informaci%C3%B3n%20sobre%20un%20servicio`; // 3. Precios en módulo de reservas de pasajeros if (window.updatePassengerBookingSummary) { window.updatePassengerBookingSummary(); } else { const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`; const totalPriceEl = document.querySelector('.total-price'); if (totalPriceEl) totalPriceEl.textContent = formattedPasajero; } // 4. Recalcular módulo de mensajería y fletes if (window.recalculateFreight) { window.recalculateFreight(); } } function formatPhoneNumber(num) { const clean = num.replace(/\D/g, ''); if (clean.startsWith('57') && clean.length === 12) { return `+57 ${clean.slice(2, 5)} ${clean.slice(5, 8)} ${clean.slice(8)}`; } return `+${clean}`; } function initTariffsManager() { const form = document.getElementById('form-tariffs-config'); const statusEl = document.getElementById('tariffs-save-status'); const btnDetect = document.getElementById('btn-detect-wa-groups'); // Auto-Detección de Grupos WhatsApp mediante Evolution API if (btnDetect) { btnDetect.addEventListener('click', async () => { btnDetect.disabled = true; const originalText = btnDetect.innerHTML; btnDetect.innerHTML = '⏳ Detectando grupos...'; try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/admin/whatsapp/groups', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (res.ok && data.success) { const inputPacific = document.getElementById('cfg-group-pacifictrans'); const inputDrip = document.getElementById('cfg-group-dripzone'); const badgePacific = document.getElementById('badge-pacific-jid'); const badgeDrip = document.getElementById('badge-dripzone-jid'); if (inputPacific && data.pacificTrans?.groupName) { inputPacific.value = data.pacificTrans.groupName; } if (badgePacific && data.pacificTrans?.resolvedJid) { badgePacific.innerHTML = `✓ Destino: ${data.pacificTrans.resolvedJid} (${data.pacificTrans.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`; badgePacific.style.color = '#34d399'; } if (inputDrip && data.dripzone?.groupName) { inputDrip.value = data.dripzone.groupName; } if (badgeDrip && data.dripzone?.resolvedJid) { badgeDrip.innerHTML = `✓ Destino: ${data.dripzone.resolvedJid} (${data.dripzone.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`; badgeDrip.style.color = '#34d399'; } alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacifictrans Go ➔ ${data.pacificTrans?.groupName} (${data.pacificTrans?.resolvedJid})\n• Dripzone ➔ ${data.dripzone?.groupName} (${data.dripzone?.resolvedJid})`); } else { alert('No se pudieron consultar los grupos: ' + (data.error || 'Verifica la sesión')); } } catch (err) { console.error('Error detect groups:', err); alert('Error al consultar Evolution API'); } finally { btnDetect.disabled = false; btnDetect.innerHTML = originalText; } }); } if (!form) return; form.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('btn-save-tariffs'); if (btn) { btn.disabled = true; btn.innerHTML = '⏳ Guardando en MongoDB...'; } const payload = { tarifaPasajeroFija: parseFloat(document.getElementById('cfg-tarifa-pasajero')?.value || '35000'), precioBasePaquete: parseFloat(document.getElementById('cfg-base-paquete')?.value || '25000'), precioKmPaquete: parseFloat(document.getElementById('cfg-km-paquete')?.value || '1200'), factorPaquetes: { sobre: parseFloat(document.getElementById('cfg-factor-sobre')?.value || '1.0'), mediano: parseFloat(document.getElementById('cfg-factor-mediano')?.value || '1.6'), grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2') }, seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'), porcentajeComisionAdmin: parseFloat(document.getElementById('cfg-comision')?.value || '15'), costoPenalidadCancelacion: parseFloat(document.getElementById('cfg-penalidad')?.value || '15000'), radioMaxPoolingMetros: parseFloat(document.getElementById('cfg-radio-pooling')?.value || '4000'), numeroAtencionWhatsApp: document.getElementById('cfg-admin-phone')?.value.trim() || '573167455877', nombreSoporte: document.getElementById('cfg-admin-name')?.value.trim() || 'Atención Pacifictrans Go', grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans Notificaciones', grupoWhatsAppDripzone: document.getElementById('cfg-group-dripzone')?.value.trim() || 'Dripzone Notificaciones' }; try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/tariffs', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); if (res.ok) { const data = await res.json(); currentTariffs = { ...currentTariffs, ...data.config }; syncTariffsToUI(); if (statusEl) { statusEl.innerHTML = `✅ ¡Configuración guardada y sincronizada! Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Grupo Pacific: "${payload.grupoWhatsAppPacificTrans}" | Grupo Dripzone: "${payload.grupoWhatsAppDripzone}"`; statusEl.style.color = '#10b981'; } } else { throw new Error('Error al guardar en el servidor'); } } catch (err) { currentTariffs = { ...currentTariffs, ...payload }; syncTariffsToUI(); if (statusEl) { statusEl.innerHTML = `✅ Configuración actualizada localmente.`; statusEl.style.color = '#10b981'; } } finally { if (btn) { btn.disabled = false; btn.innerHTML = '💾 Guardar y Aplicar Configuración'; } } }); } /* ========================================================================== 7. RESERVA DE PASAJEROS & POOLING ========================================================================== */ function initPassengerCalculator() { const originSelect = document.getElementById('pool-origin'); const addressGroup = document.getElementById('cali-address-group'); const buenaStopGroup = document.getElementById('buena-stop-group'); const caliAddressInput = document.getElementById('cali-address'); const buenaStopSelect = document.getElementById('buena-stop'); const bookBtn = document.getElementById('btn-book-pool'); const poolDots = document.querySelectorAll('.seat-dot'); const passengersContainer = document.getElementById('passengers-cards-container'); const badgeCupos = document.getElementById('badge-cupos-selected'); const seatButtons = document.querySelectorAll('.btn-qty-seat'); const paymentCards = document.querySelectorAll('.payment-method-card'); let selectedSeats = 1; let selectedPaymentMethod = 'BOLD'; let currentSeatsFilled = 3; // Cache para persistir los datos ingresados al alternar entre 1..4 cupos const passengersData = [ { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }, { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }, { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }, { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' } ]; function saveCurrentFormData() { for (let i = 1; i <= selectedSeats; i++) { const nameInput = document.getElementById(`passenger-${i}-name`); const docInput = document.getElementById(`passenger-${i}-doc`); const phoneInput = document.getElementById(`passenger-${i}-phone`); const notesInput = document.getElementById(`passenger-${i}-notes`); const sameAddrCheck = document.getElementById(`passenger-${i}-same-addr`); const addrInput = document.getElementById(`passenger-${i}-addr`); if (nameInput) passengersData[i - 1].nombre = nameInput.value; if (docInput) passengersData[i - 1].doc = docInput.value; if (phoneInput) passengersData[i - 1].phone = phoneInput.value; if (notesInput) passengersData[i - 1].notes = notesInput.value; if (sameAddrCheck) passengersData[i - 1].sameAddress = sameAddrCheck.checked; if (addrInput) passengersData[i - 1].address = addrInput.value; } } function renderPassengerCards(count) { if (!passengersContainer) return; // Si hay usuario logueado y el pasajero 1 no tiene nombre, prellenar if (currentUser && !passengersData[0].nombre) { passengersData[0].nombre = currentUser.nombre || ''; passengersData[0].phone = currentUser.telefono || ''; } let html = ''; for (let i = 1; i <= count; i++) { const isTitular = (i === 1); const data = passengersData[i - 1]; html += `
${isTitular ? '👤' : '👥'} Pasajero ${i} ${isTitular ? '(Titular de la Reserva)' : `(Acompañante #${i - 1})`}
Cupo #${i} • ${isTitular ? 'Titular' : 'Acompañante'}
${!isTitular ? `
` : ''}
`; } passengersContainer.innerHTML = html; // Conectar eventos a los checkboxes de misma dirección for (let i = 2; i <= count; i++) { const check = document.getElementById(`passenger-${i}-same-addr`); const addrGroup = document.getElementById(`passenger-${i}-addr-group`); if (check && addrGroup) { check.addEventListener('change', () => { addrGroup.style.display = check.checked ? 'none' : 'block'; passengersData[i - 1].sameAddress = check.checked; }); } } } let appliedCoupon = null; const poolCouponInput = document.getElementById('pool-coupon-code'); const btnApplyPoolCoupon = document.getElementById('btn-apply-pool-coupon'); const poolCouponFeedback = document.getElementById('pool-coupon-feedback'); if (btnApplyPoolCoupon && poolCouponInput) { btnApplyPoolCoupon.addEventListener('click', async () => { const code = poolCouponInput.value.trim().toUpperCase(); if (!code) { if (poolCouponFeedback) { poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error'; poolCouponFeedback.textContent = '⚠️ Ingresa el código del bono o cupón.'; poolCouponFeedback.style.display = 'flex'; } return; } btnApplyPoolCoupon.disabled = true; btnApplyPoolCoupon.textContent = '...'; try { const subtotal = selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000); const res = await fetch('/api/coupons/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ codigo: code, serviceType: 'VIAJES', subtotal }) }); const data = await res.json(); if (data.valid) { appliedCoupon = data; if (poolCouponFeedback) { poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-success'; poolCouponFeedback.textContent = data.message; poolCouponFeedback.style.display = 'flex'; } } else { appliedCoupon = null; if (poolCouponFeedback) { poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error'; poolCouponFeedback.textContent = data.message || 'Cupón inválido'; poolCouponFeedback.style.display = 'flex'; } } updateSummary(); } catch (err) { console.error('Coupon validation error:', err); if (poolCouponFeedback) { poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error'; poolCouponFeedback.textContent = 'Error al validar cupón con el servidor.'; poolCouponFeedback.style.display = 'flex'; } } finally { btnApplyPoolCoupon.disabled = false; btnApplyPoolCoupon.textContent = 'Aplicar'; } }); } function updateSummary() { const unitPrice = currentTariffs.tarifaPasajeroFija || 35000; const subtotal = selectedSeats * unitPrice; let discountAmount = 0; if (appliedCoupon && appliedCoupon.valid) { if (appliedCoupon.coupon.tipoDescuento === 'PORCENTAJE') { discountAmount = Math.round(subtotal * (appliedCoupon.coupon.valorDescuento / 100)); } else { discountAmount = Math.min(appliedCoupon.coupon.valorDescuento, subtotal); } discountAmount = Math.min(discountAmount, subtotal); } const totalAmount = Math.max(0, subtotal - discountAmount); const formattedUnit = `$${unitPrice.toLocaleString('es-CO')} COP`; const formattedSubtotal = `$${subtotal.toLocaleString('es-CO')} COP`; const formattedTotal = `$${totalAmount.toLocaleString('es-CO')} COP`; const summarySubtotalRow = document.getElementById('summary-subtotal-row'); const summarySubtotalPrice = document.getElementById('summary-subtotal-price'); const summaryDiscountRow = document.getElementById('summary-discount-row'); const summaryDiscountAmount = document.getElementById('summary-discount-amount'); const summaryPriceLabel = document.getElementById('summary-price-label'); const summaryTotalPrice = document.getElementById('summary-total-price'); const summaryCapacity = document.getElementById('summary-capacity-text'); const summarySeatsPill = document.getElementById('summary-seats-pill'); const btnBookText = document.getElementById('btn-book-text'); const badgeCuposSelected = document.getElementById('badge-cupos-selected'); if (badgeCuposSelected) { badgeCuposSelected.textContent = `${selectedSeats} ${selectedSeats === 1 ? 'Cupo Seleccionado' : 'Cupos Seleccionados'}`; } if (summarySeatsPill) { summarySeatsPill.textContent = `${selectedSeats} ${selectedSeats === 1 ? 'Pasajero (1 cupo)' : `Pasajeros (${selectedSeats} cupos)`}`; } if (summaryCapacity) { summaryCapacity.textContent = selectedSeats === 4 ? 'Vehículo Completo Exclusivo (4/4 Cupos)' : `Máximo 4 Pasajeros (${selectedSeats} ${selectedSeats === 1 ? 'cupo solicitado' : 'cupos solicitados'})`; } if (discountAmount > 0) { if (summarySubtotalRow) { summarySubtotalRow.style.display = 'flex'; if (summarySubtotalPrice) summarySubtotalPrice.textContent = formattedSubtotal; } if (summaryDiscountRow) { summaryDiscountRow.style.display = 'flex'; const typeText = appliedCoupon.coupon.tipoDescuento === 'PORCENTAJE' ? `${appliedCoupon.coupon.valorDescuento}% OFF` : 'Bono Fijo'; if (summaryDiscountAmount) summaryDiscountAmount.textContent = `- $${discountAmount.toLocaleString('es-CO')} COP (${typeText})`; } if (summaryPriceLabel) { summaryPriceLabel.textContent = `Total con Descuento (${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'}):`; } } else { if (summarySubtotalRow) summarySubtotalRow.style.display = 'none'; if (summaryDiscountRow) summaryDiscountRow.style.display = 'none'; if (summaryPriceLabel) { summaryPriceLabel.textContent = `Total a Pagar (${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'}):`; } } if (summaryTotalPrice) { summaryTotalPrice.textContent = formattedTotal; } if (btnBookText) { const methodText = selectedPaymentMethod === 'BOLD' ? 'Pagar con Bold' : (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar'); btnBookText.textContent = `${methodText} ${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'} (${formattedTotal})`; } // Actualizar previsualización en dots del vehículo updateSeatVisuals(selectedSeats); } window.updatePassengerBookingSummary = updateSummary; // Escuchadores de botones de selección de cupos seatButtons.forEach((btn) => { btn.addEventListener('click', () => { saveCurrentFormData(); seatButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedSeats = parseInt(btn.getAttribute('data-seats') || '1', 10); renderPassengerCards(selectedSeats); updateSummary(); }); }); // Selector de método de pago paymentCards.forEach((card) => { card.addEventListener('click', () => { paymentCards.forEach(c => c.classList.remove('active')); card.classList.add('active'); const radio = card.querySelector('input[type="radio"]'); if (radio) { radio.checked = true; selectedPaymentMethod = radio.value; } updateSummary(); }); }); function updateOriginDestination() { if (!originSelect) return; const isCali = originSelect.value === 'CALI'; if (addressGroup) addressGroup.style.display = isCali ? 'block' : 'none'; if (buenaStopGroup) buenaStopGroup.style.display = isCali ? 'block' : 'none'; } if (originSelect) { originSelect.addEventListener('change', updateOriginDestination); updateOriginDestination(); } // Render inicial renderPassengerCards(selectedSeats); updateSummary(); // Envío del Formulario y Reserva en MongoDB con Pooling if (bookBtn) { bookBtn.addEventListener('click', async (e) => { e.preventDefault(); saveCurrentFormData(); // 1. Validar dirección principal de Cali const caliAddress = caliAddressInput ? caliAddressInput.value.trim() : ''; const isCaliOrigin = originSelect ? originSelect.value === 'CALI' : true; if (isCaliOrigin && !caliAddress) { alert('📍 Por favor ingresa la dirección de recogida en Cali (domicilio puerta a puerta).'); caliAddressInput?.focus(); return; } // 2. Validar que cada pasajero tenga nombre, documento y teléfono const validatedPassengers = []; for (let i = 1; i <= selectedSeats; i++) { const d = passengersData[i - 1]; if (!d.nombre || d.nombre.trim().length < 3) { alert(`⚠️ Por favor escribe el nombre completo del Pasajero #${i}.`); document.getElementById(`passenger-${i}-name`)?.focus(); return; } if (!d.doc || d.doc.trim().length < 5) { alert(`⚠️ Por favor ingresa el documento de identidad del Pasajero #${i} (requerido para la póliza contractual SOAT).`); document.getElementById(`passenger-${i}-doc`)?.focus(); return; } if (!d.phone || d.phone.trim().replace(/\D/g, '').length < 7) { alert(`⚠️ Por favor ingresa un número de teléfono o WhatsApp válido para el Pasajero #${i}.`); document.getElementById(`passenger-${i}-phone`)?.focus(); return; } const pAddress = (!d.sameAddress && d.address && d.address.trim()) ? d.address.trim() : (caliAddress || 'Punto Oficial Buenaventura'); validatedPassengers.push({ nombre: d.nombre.trim(), documentoIdentidad: d.doc.trim(), telefono: d.phone.trim(), direccionRecogida: pAddress, coordenadas: [-76.5320, 3.4215], notas: d.notes ? d.notes.trim() : '', esTitular: (i === 1) }); } // 3. Verificación de seguridad OTP if (!currentUser) { const p1Phone = validatedPassengers[0].telefono.replace(/\D/g, ''); const authInput = document.getElementById('auth-identifier'); if (authInput && p1Phone) { authInput.value = p1Phone; } alert('🛡️ Por seguridad de los conductores y de los demás pasajeros, debes verificar tu identidad con OTP antes de confirmar tu reserva.'); document.getElementById('modal-auth-dripzone')?.classList.add('active'); return; } // 4. Procesar reserva en el Backend bookBtn.disabled = true; const originalBtnHtml = bookBtn.innerHTML; bookBtn.innerHTML = ` Agrupando ${selectedSeats} Cupo(s)... `; try { const payload = { usuarioId: currentUser ? currentUser._id : undefined, sentido: isCaliOrigin ? 'CALI_A_BUENAVENTURA' : 'BUENAVENTURA_A_CALI', direccionTexto: caliAddress || 'Terminal Buenaventura', coordenadas: [-76.5320, 3.4215], puntoFijoBuenaventura: { nombre: buenaStopSelect ? buenaStopSelect.options[buenaStopSelect.selectedIndex].text : 'Terminal de Transporte de Buenaventura', coordenadas: [-77.0250, 3.8820] }, metodoPago: selectedPaymentMethod, cuposSolicitados: selectedSeats, pasajeros: validatedPassengers, codigoCupon: appliedCoupon?.coupon?.codigo || undefined, email: currentUser?.email || '' }; const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/trips/pool', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok && data.success) { const totalOccupied = data.totalCuposOcupados || data.pasajerosAgrupados || 4; updateSeatVisuals(totalOccupied); // Mostrar alerta detallada const alertBox = document.getElementById('pool-dispatch-alert'); const alertTitle = document.getElementById('alert-title'); const alertMsg = document.getElementById('alert-message'); const alertBreakdown = document.getElementById('alert-passengers-breakdown'); const alertBoldArea = document.getElementById('alert-bold-payment-area'); if (alertTitle) { alertTitle.textContent = totalOccupied === 4 ? '🔥 ¡Vehículo Completo (4/4 Cupos) Listo para Despacho!' : `✅ ¡${selectedSeats} ${selectedSeats === 1 ? 'Cupo Reservado' : 'Cupos Reservados'} Exitosamente! (${totalOccupied}/4 Ocupados)`; } if (alertMsg) { alertMsg.innerHTML = ` Viaje #${data.tripId?.slice(-6) || ''} en ruta ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}.
${data.message} `; } if (alertBreakdown) { let breakdownHtml = `
👥 Pasajeros Registrados en esta Reserva:
`; validatedPassengers.forEach((p, idx) => { breakdownHtml += `
${idx + 1}. ${p.nombre} (C.C. ${p.documentoIdentidad}) • Tel: +${p.telefono} • Recogida: ${p.direccionRecogida} ${p.notas ? `• 🧳 ${p.notas}` : ''}
`; }); breakdownHtml += `
`; alertBreakdown.innerHTML = breakdownHtml; } // Si es pago Bold, renderizar botón y QR if (alertBoldArea) { if (data.boldPayment && data.boldPayment.checkoutUrl) { alertBoldArea.style.display = 'block'; alertBoldArea.innerHTML = `
💳 Enlace de Pago Bold Smart Checkout Generado

Total a pagar: $${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP por ${selectedSeats} cupo(s).

👉 Abrir Pasarela Bold (Pagar con QR Bre-B, Nequi o PSE)
`; } else { alertBoldArea.style.display = 'none'; } } if (alertBox) { alertBox.style.display = 'block'; alertBox.scrollIntoView({ behavior: 'smooth' }); } const formattedTotal = `$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP`; bookBtn.innerHTML = `✅ ${selectedSeats} Cupo(s) Confirmado(s) (${formattedTotal})`; } else { alert('❌ No se pudo completar la reserva: ' + (data.message || data.error || 'Intenta de nuevo')); bookBtn.innerHTML = originalBtnHtml; bookBtn.disabled = false; } } catch (err) { console.error('Error booking pool:', err); alert('⚠️ Error de conexión con el servidor de pooling. Verifica tu red.'); bookBtn.innerHTML = originalBtnHtml; bookBtn.disabled = false; } }); } function updateSeatVisuals(count) { poolDots.forEach((dot, index) => { dot.classList.remove('active', 'pending'); if (index < count) { dot.classList.add('active'); } else if (index === count) { dot.classList.add('pending'); } }); const seatCounter = document.getElementById('seat-counter-text'); if (seatCounter) seatCounter.textContent = `${count}/4 Pasajeros Confirmados`; } updateSeatVisuals(currentSeatsFilled); } /* ========================================================================== 8. CALCULADORA DE MENSAJERÍA & FLETES ========================================================================== */ function initFreightCalculator() { const packageTypeSelect = document.getElementById('package-type'); const declaredValueInput = document.getElementById('declared-value'); const totalPriceEl = document.getElementById('freight-total-price'); const baseCostEl = document.getElementById('freight-base-cost'); const distCostEl = document.getElementById('freight-dist-cost'); const insuranceCostEl = document.getElementById('freight-insurance-cost'); const freightSubtotalRow = document.getElementById('freight-subtotal-row'); const freightSubtotalPrice = document.getElementById('freight-subtotal-price'); const freightDiscountRow = document.getElementById('freight-discount-row'); const freightDiscountAmount = document.getElementById('freight-discount-amount'); const freightCouponInput = document.getElementById('freight-coupon-code'); const btnApplyFreightCoupon = document.getElementById('btn-apply-freight-coupon'); const freightCouponFeedback = document.getElementById('freight-coupon-feedback'); const btnBookFreight = document.getElementById('btn-book-freight'); const KM_DIST = 115; // Corredor Cali - Buenaventura let appliedFreightCoupon = null; window.recalculateFreight = function () { const rawFactor = parseFloat(packageTypeSelect?.value || '1.0'); let factor = rawFactor; if (rawFactor === 1.0 && currentTariffs.factorPaquetes?.sobre) { factor = currentTariffs.factorPaquetes.sobre; } else if (rawFactor === 1.6 && currentTariffs.factorPaquetes?.mediano) { factor = currentTariffs.factorPaquetes.mediano; } else if (rawFactor === 2.2 && currentTariffs.factorPaquetes?.grande) { factor = currentTariffs.factorPaquetes.grande; } const declaredValue = parseFloat(declaredValueInput?.value || '0'); const basePrice = currentTariffs.precioBasePaquete || 25000; const costPerKm = currentTariffs.precioKmPaquete || 1200; const distancePart = KM_DIST * costPerKm; const subtotal = (basePrice + distancePart) * factor; const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0); const subtotalBruto = Math.round(subtotal + seguro); let discountAmount = 0; if (appliedFreightCoupon && appliedFreightCoupon.valid) { if (appliedFreightCoupon.coupon.tipoDescuento === 'PORCENTAJE') { discountAmount = Math.round(subtotalBruto * (appliedFreightCoupon.coupon.valorDescuento / 100)); } else { discountAmount = Math.min(appliedFreightCoupon.coupon.valorDescuento, subtotalBruto); } discountAmount = Math.min(discountAmount, subtotalBruto); } const total = Math.max(0, subtotalBruto - discountAmount); if (baseCostEl) baseCostEl.textContent = `$${basePrice.toLocaleString('es-CO')} COP`; if (distCostEl) distCostEl.textContent = `$${Math.round(distancePart * factor).toLocaleString('es-CO')} COP`; if (insuranceCostEl) insuranceCostEl.textContent = `$${seguro.toLocaleString('es-CO')} COP`; if (discountAmount > 0) { if (freightSubtotalRow) { freightSubtotalRow.style.display = 'flex'; if (freightSubtotalPrice) freightSubtotalPrice.textContent = `$${subtotalBruto.toLocaleString('es-CO')} COP`; } if (freightDiscountRow) { freightDiscountRow.style.display = 'flex'; const typeText = appliedFreightCoupon.coupon.tipoDescuento === 'PORCENTAJE' ? `${appliedFreightCoupon.coupon.valorDescuento}% OFF` : 'Bono Fijo'; if (freightDiscountAmount) freightDiscountAmount.textContent = `- $${discountAmount.toLocaleString('es-CO')} COP (${typeText})`; } } else { if (freightSubtotalRow) freightSubtotalRow.style.display = 'none'; if (freightDiscountRow) freightDiscountRow.style.display = 'none'; } if (totalPriceEl) totalPriceEl.textContent = `$${total.toLocaleString('es-CO')} COP`; }; if (packageTypeSelect) packageTypeSelect.addEventListener('change', window.recalculateFreight); if (declaredValueInput) declaredValueInput.addEventListener('input', window.recalculateFreight); if (btnApplyFreightCoupon && freightCouponInput) { btnApplyFreightCoupon.addEventListener('click', async () => { const code = freightCouponInput.value.trim().toUpperCase(); if (!code) { if (freightCouponFeedback) { freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error'; freightCouponFeedback.textContent = '⚠️ Ingresa el código del bono o cupón de envío.'; freightCouponFeedback.style.display = 'flex'; } return; } btnApplyFreightCoupon.disabled = true; btnApplyFreightCoupon.textContent = '...'; try { const rawFactor = parseFloat(packageTypeSelect?.value || '1.0'); const declaredValue = parseFloat(declaredValueInput?.value || '0'); const basePrice = currentTariffs.precioBasePaquete || 25000; const costPerKm = currentTariffs.precioKmPaquete || 1200; const distancePart = KM_DIST * costPerKm; const subtotal = (basePrice + distancePart) * rawFactor; const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0); const subtotalBruto = Math.round(subtotal + seguro); const res = await fetch('/api/coupons/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ codigo: code, serviceType: 'ENCOMIENDAS', subtotal: subtotalBruto }) }); const data = await res.json(); if (data.valid) { appliedFreightCoupon = data; if (freightCouponFeedback) { freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-success'; freightCouponFeedback.textContent = data.message; freightCouponFeedback.style.display = 'flex'; } } else { appliedFreightCoupon = null; if (freightCouponFeedback) { freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error'; freightCouponFeedback.textContent = data.message || 'Cupón no válido para encomiendas'; freightCouponFeedback.style.display = 'flex'; } } window.recalculateFreight(); } catch (err) { console.error('Coupon freight error:', err); if (freightCouponFeedback) { freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error'; freightCouponFeedback.textContent = 'Error al verificar cupón.'; freightCouponFeedback.style.display = 'flex'; } } finally { btnApplyFreightCoupon.disabled = false; btnApplyFreightCoupon.textContent = 'Aplicar'; } }); } // Despacho / Creación de Encomienda con Bold y Cupón if (btnBookFreight) { btnBookFreight.addEventListener('click', async (e) => { e.preventDefault(); const senderPhone = document.getElementById('sender-phone')?.value.trim(); const receiverPhone = document.getElementById('receiver-phone')?.value.trim(); const packageDesc = document.getElementById('package-desc')?.value.trim(); if (!senderPhone || senderPhone.replace(/\D/g, '').length < 7) { alert('⚠️ Por favor ingresa el teléfono/WhatsApp del remitente.'); document.getElementById('sender-phone')?.focus(); return; } if (!receiverPhone || receiverPhone.replace(/\D/g, '').length < 7) { alert('⚠️ Por favor ingresa el teléfono/WhatsApp del destinatario.'); document.getElementById('receiver-phone')?.focus(); return; } if (!packageDesc) { alert('⚠️ Por favor describe el contenido de la encomienda.'); document.getElementById('package-desc')?.focus(); return; } btnBookFreight.disabled = true; const originalText = btnBookFreight.innerHTML; btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Bold...'; try { const payload = { tipoPaquete: packageTypeSelect ? packageTypeSelect.options[packageTypeSelect.selectedIndex].text : 'Paquete Pequeño', factor: parseFloat(packageTypeSelect?.value || '1.2'), valorDeclarado: parseFloat(declaredValueInput?.value || '100000'), telefonoRemitente: senderPhone, telefonoDestinatario: receiverPhone, descripcionContenido: packageDesc, codigoCupon: appliedFreightCoupon?.coupon?.codigo || undefined }; const res = await fetch('/api/trips/package', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok && data.success) { if (data.boldPaymentLink) { window.open(data.boldPaymentLink, '_blank'); alert(`✅ ¡Guía #${data.guiaSeguimiento || 'PT'} generada!\n\nTarifa Liquidada: $${(data.tarifaFinal || data.tarifaCalculada).toLocaleString('es-CO')} COP\n${data.descuentoCupon ? '🎟️ Descuento aplicado: -$' + data.descuentoCupon.descuento.toLocaleString('es-CO') + ' COP\n' : ''}\nSe ha abierto la pasarela Bold Smart Checkout para completar el pago.`); } else { alert(`✅ Guía generada correctamente: #${data.guiaSeguimiento}`); } } else { alert('Error al generar envío: ' + (data.error || 'Verifica los datos')); } } catch (err) { console.error('Error package booking:', err); alert('Error al conectar con el servidor.'); } finally { btnBookFreight.disabled = false; btnBookFreight.innerHTML = originalText; } }); } } /* ========================================================================== 9. CENTRO DE NOTIFICACIONES & CONTADOR DE MENSAJES PENDIENTES ========================================================================== */ function initNotificationsSystem() { const btnBell = document.getElementById('btn-toggle-notifs'); const dropdown = document.getElementById('notif-dropdown-panel'); const badge = document.getElementById('notif-unread-count'); const headerUnread = document.getElementById('notif-header-unread'); const listEl = document.getElementById('notif-items-list'); const btnMarkAll = document.getElementById('btn-mark-all-read'); const waLink = document.getElementById('notif-wa-link'); const defaultNotifs = [ { id: '1', icon: '🚗', title: '¡Cupo 4/4 Completado!', desc: 'Tu viaje Cali ➔ Buenaventura tiene conductor asignado (Mario Fernando G.). Recogida escalonada y ruta Waze TSP activa.', time: 'Hace 5 min', unread: true }, { id: '2', icon: '📦', title: 'Encomienda en Ruta 40', desc: 'Guía #PT-8921 despachada desde Cali con destino a Buenaventura. Seguimiento satelital en vivo.', time: 'Hace 25 min', unread: true }, { id: '3', icon: '🛡️', title: 'Identidad Verificada 2FA', desc: 'Tu cuenta ha sido validada con WhatsApp OTP. Viajes y envíos 100% seguros y respaldados.', time: 'Hace 1 hora', unread: true } ]; let stored = localStorage.getItem('pt_notifications'); let notifications = stored ? JSON.parse(stored) : defaultNotifs; function saveAndRender() { localStorage.setItem('pt_notifications', JSON.stringify(notifications)); const unreadCount = notifications.filter(n => n.unread).length; if (badge) { badge.textContent = unreadCount; badge.style.display = unreadCount > 0 ? 'flex' : 'none'; } if (headerUnread) { headerUnread.textContent = `${unreadCount} nuevas`; headerUnread.style.display = unreadCount > 0 ? 'inline-block' : 'none'; } if (listEl) { if (notifications.length === 0) { listEl.innerHTML = '
No tienes notificaciones pendientes.
'; } else { listEl.innerHTML = notifications.map(n => `
${n.icon}
${n.title}
${n.desc}
${n.time}
`).join(''); listEl.querySelectorAll('.notif-item').forEach(item => { item.addEventListener('click', () => { const id = item.getAttribute('data-id'); const target = notifications.find(n => n.id === id); if (target) { target.unread = false; saveAndRender(); } }); }); } } } if (btnBell && dropdown) { btnBell.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('active'); }); document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && !btnBell.contains(e.target)) { dropdown.classList.remove('active'); } }); } if (btnMarkAll) { btnMarkAll.addEventListener('click', () => { notifications.forEach(n => { n.unread = false; }); saveAndRender(); }); } // Sincronizar enlace de WhatsApp con el número configurado dinámicamente if (waLink && currentTariffs.numeroAtencionWhatsApp) { const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, ''); waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacifictrans%20Go`; } saveAndRender(); } /* ========================================================================== 10. CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS INMEDIATAS BRE-B ========================================================================== */ function initSettlementsAndBreBSystem() { const defaultDriverId = '6528f1100000000000000001'; let activeTripData = null; let currentDriverWallet = null; // Modal elements const brebModal = document.getElementById('modal-breb-receipt'); const btnCloseBrebModal = document.getElementById('btn-close-breb-modal'); const btnCopyBrebRef = document.getElementById('btn-copy-breb-receipt'); const btnShareBrebWa = document.getElementById('btn-share-breb-whatsapp'); // Helper para abrir el comprobante oficial de Bre-B window.openBreBReceiptModal = function (receipt) { if (!brebModal) return; const amountEl = document.getElementById('modal-receipt-amount'); const datetimeEl = document.getElementById('modal-receipt-datetime'); const refEl = document.getElementById('modal-receipt-ref'); const authEl = document.getElementById('modal-receipt-auth'); const cycleEl = document.getElementById('modal-receipt-cycle'); const bankEl = document.getElementById('modal-receipt-bank'); const keyEl = document.getElementById('modal-receipt-key'); const ownerEl = document.getElementById('modal-receipt-owner'); if (amountEl) amountEl.textContent = `$${(receipt.monto || 0).toLocaleString('es-CO')} COP`; if (datetimeEl) datetimeEl.textContent = receipt.fechaHora || new Date().toLocaleString('es-CO'); if (refEl) refEl.textContent = receipt.referenciaBreB || 'BREB-CO-20261014-94F2B0'; if (authEl) authEl.textContent = receipt.codigoAutorizacion || 'AUT-98E1A4'; if (cycleEl) cycleEl.textContent = receipt.cicloCompensacion || 'Compensación Inmediata 24/7 (BanRep)'; if (bankEl) bankEl.textContent = receipt.entidadDestino || 'Bancolombia'; if (keyEl) keyEl.textContent = receipt.llaveDestino || '+57 316 745 5877'; if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Carlos Almeira'; brebModal.style.display = 'flex'; if (btnCopyBrebRef) { btnCopyBrebRef.onclick = () => { navigator.clipboard?.writeText(receipt.referenciaBreB || ''); btnCopyBrebRef.textContent = '✓ ¡Copiado!'; setTimeout(() => { btnCopyBrebRef.textContent = '📋 Copiar Referencia'; }, 2000); }; } if (btnShareBrebWa) { btnShareBrebWa.onclick = () => { const text = `🏦 *COMPROBANTE OFICIAL TRANSFERENCIA INMEDIATA BRE-B*\n` + `_Banco de la República - Red Nacional Interoperable_\n\n` + `✅ *Estado:* Aprobada Inmediata 24/7\n` + `📄 *Referencia:* ${receipt.referenciaBreB}\n` + `💵 *Monto:* $${(receipt.monto || 0).toLocaleString('es-CO')} COP\n` + `🏦 *Entidad Destino:* ${receipt.entidadDestino}\n` + `🔑 *Llave:* ${receipt.llaveDestino}\n` + `💸 *Comisión:* $0 COP (Bre-B Gratuito)\n\n` + `_Pacifictrans Go • Pagos Seguros en Tiempo Real_`; window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank'); }; } }; if (btnCloseBrebModal && brebModal) { btnCloseBrebModal.addEventListener('click', () => { brebModal.style.display = 'none'; }); brebModal.addEventListener('click', (e) => { if (e.target === brebModal) brebModal.style.display = 'none'; }); } /* -------------------------------------------------------------------------- A. PANEL ADMINISTRADOR: OVERVIEW & CONTROL DE PAGOS DE VIAJE -------------------------------------------------------------------------- */ window.fetchSettlementsOverview = async function () { try { const res = await fetch('/api/settlements/overview'); const data = await res.json(); if (data.success) { // Actualizar KPIs de administración const mPlataforma = document.getElementById('metric-recaudo-plataforma'); const mEfectivo = document.getElementById('metric-recaudo-efectivo'); const mComision = document.getElementById('metric-comision-total'); const mBreb = document.getElementById('metric-transferido-breb'); let recPlataforma = 0; let recEfectivo = 0; let comTotal = 0; (data.viajesProgramadosYActivos || []).forEach(v => { (v.pasajeros || []).forEach(p => { if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago)) { recPlataforma += p.valor || 35000; } else if (p.estadoPago === 'PAGADO_EFECTIVO') { recEfectivo += p.valor || 35000; } }); comTotal += v.comisionAdmin || 0; }); if (mPlataforma) mPlataforma.textContent = `$${recPlataforma.toLocaleString('es-CO')}`; if (mEfectivo) mEfectivo.textContent = `$${recEfectivo.toLocaleString('es-CO')}`; if (mComision) mComision.textContent = `$${comTotal.toLocaleString('es-CO')}`; if (mBreb) mBreb.textContent = `$${(data.metricas?.totalTransferidoBreB || 0).toLocaleString('es-CO')}`; // Llenar selector de viajes const selectTrip = document.getElementById('select-trip-to-settle'); if (selectTrip) { selectTrip.innerHTML = ''; const trips = data.viajesProgramadosYActivos || []; if (trips.length > 0) { trips.forEach((t, idx) => { const opt = document.createElement('option'); opt.value = t._id; const condName = t.conductorId?.nombre || 'Conductor asignado'; opt.textContent = `Viaje #${t._id.slice(-6)} (${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'B/ventura'}) - ${condName}`; selectTrip.appendChild(opt); }); selectTrip.value = trips[0]._id; loadTripSettlement(trips[0]._id); } else { // Generar opción demo si la BD aún no tiene viajes activos creados const optDemo = document.createElement('option'); optDemo.value = 'DEMO_TRIP_001'; optDemo.textContent = 'Viaje Planificado #048291 (Cali ➔ Buenaventura) - Carlos Almeira (WLZ-890)'; selectTrip.appendChild(optDemo); loadTripSettlement('DEMO_TRIP_001'); } selectTrip.onchange = () => { loadTripSettlement(selectTrip.value); }; } // Renderizar historial de liquidaciones renderSettlementsHistory(data.viajesCompletadosRecientes || []); } } catch (err) { console.warn('⚠️ [Settlements Warning]:', err); // Fallback amigable si se está probando offline loadTripSettlement('DEMO_TRIP_001'); } }; const btnRefreshSettlements = document.getElementById('btn-refresh-settlements'); if (btnRefreshSettlements) { btnRefreshSettlements.addEventListener('click', () => { window.fetchSettlementsOverview(); }); } // Cargar detalle y estado de pago de un viaje async function loadTripSettlement(tripId) { const container = document.getElementById('trip-settlement-detail-container'); if (!container) return; container.innerHTML = '
Cargando liquidación y estado de pasajeros...
'; let summary = null; if (tripId && tripId !== 'DEMO_TRIP_001') { try { const res = await fetch(`/api/settlements/trip/${tripId}`); const data = await res.json(); if (data.success) { summary = data.summary; } } catch (e) { console.warn('Error fetching trip summary:', e); } } // Datos por defecto enriquecidos si no existe viaje en BD aún if (!summary) { summary = { tripId: 'DEMO_TRIP_001', estadoViaje: 'SCHEDULED', origenCiudad: 'CALI', destinoCiudad: 'BUENAVENTURA', fechaSalida: new Date(Date.now() + 3600000), conductor: { id: defaultDriverId, nombre: 'Carlos Almeira', telefono: '573167455877', placa: 'WLZ-890', modelo: 'Renault Duster 2024', llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' } }, tarifaPorPasajero: 35000, tarifaTotal: 140000, porcentajeComision: 15, comisionAdmin: 21000, pagoNetoConductor: 119000, totalRecaudadoPlataforma: 70000, totalRecaudadoEfectivo: 35000, totalPendienteEfectivo: 35000, saldoNetoParaConductor: 49000, saldoComisionParaPlataforma: 0, estadoLiquidacion: 'PENDIENTE', pasajeros: [ { pasajeroId: 'P1', nombre: 'Juan Camilo Pérez', telefono: '3168923412', direccionRecogida: 'Cra. 5 #12-45, Barrio Granada, Cali', estadoPago: 'PAGADO_BOLD', valor: 35000, esRecaudadoPlataforma: true, esRecaudadoEfectivo: false, esPendienteEfectivo: false }, { pasajeroId: 'P2', nombre: 'María Camila Benítez', telefono: '3157201948', direccionRecogida: 'Calle 5 #27-18, San Fernando, Cali', estadoPago: 'PAGADO_BRE_B', valor: 35000, esRecaudadoPlataforma: true, esRecaudadoEfectivo: false, esPendienteEfectivo: false }, { pasajeroId: 'P3', nombre: 'Andrés Mauricio Castro', telefono: '3184490123', direccionRecogida: 'Av. 6N #22-10, Versalles, Cali', estadoPago: 'PAGADO_EFECTIVO', valor: 35000, esRecaudadoPlataforma: false, esRecaudadoEfectivo: true, esPendienteEfectivo: false }, { pasajeroId: 'P4', nombre: 'Diana Marcela Riascos', telefono: '3176059281', direccionRecogida: 'Calle 9 #44-30, Los Cámbulos, Cali', estadoPago: 'PENDIENTE_EFECTIVO', valor: 35000, esRecaudadoPlataforma: false, esRecaudadoEfectivo: false, esPendienteEfectivo: true } ] }; } activeTripData = summary; renderTripSettlementDetail(summary); } // Renderizar la vista completa del viaje seleccionado function renderTripSettlementDetail(s) { const container = document.getElementById('trip-settlement-detail-container'); if (!container) return; const fechaSalidaStr = s.fechaSalida ? new Date(s.fechaSalida).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true }) : 'Pendiente'; const estadoBadgeHtml = s.estadoViaje === 'COMPLETED' ? '✓ VIAJE COMPLETADO & LIQUIDADO' : '🚗 EN CURSO / PROGRAMADO'; let html = `
Ruta: ${s.origenCiudad} ➔ ${s.destinoCiudad} (#${s.tripId.slice(-6)}) ${estadoBadgeHtml}
Salida Programada: ${fechaSalidaStr} • Conductor: ${s.conductor?.nombre || 'Carlos Almeira'} (${s.conductor?.placa || 'WLZ-890'})
Llave Bre-B del Conductor:
${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3167455877'}

👥 Control de Pasajeros & Recaudos Individuales (Cupo 4/4)

`; s.pasajeros.forEach((p, idx) => { let badgeHtml = ''; let actionBtnHtml = ''; if (p.estadoPago === 'PAGADO_BOLD') { badgeHtml = '💳 Pagado Bold'; } else if (p.estadoPago === 'PAGADO_NEQUI') { badgeHtml = '📱 Pagado Nequi'; } else if (p.estadoPago === 'PAGADO_BRE_B') { badgeHtml = '⚡ Pagado Bre-B'; } else if (p.estadoPago === 'PAGADO_EFECTIVO') { badgeHtml = '✓ Pagado en Efectivo'; actionBtnHtml = ` `; } else { badgeHtml = '⏳ Pendiente Efectivo al Abordar'; actionBtnHtml = ` `; } html += `
${idx + 1}. ${p.nombre} (+${p.telefono}) ${p.documentoIdentidad ? `C.C. ${p.documentoIdentidad}` : ''} ${p.esTitular ? 'Titular' : ''}
📍 Recogida: ${p.direccionRecogida} ${p.notas ? `
🧳 Equipaje: ${p.notas}` : ''}
Tarifa Cupo
$${(p.valor || 35000).toLocaleString('es-CO')}
${badgeHtml}
${actionBtnHtml}
`; }); html += `

⚖️ Liquidación Financiera del Recorrido & Split Conductor/Plataforma

Recaudo Total (4 Cupos)
$${s.tarifaTotal.toLocaleString('es-CO')}
100% de la Ruta
Recaudado Plataforma
$${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}
Online Bold / Bre-B
Recaudado en Efectivo
$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}
Cobrado por Conductor
Comisión Pacifictrans (${s.porcentajeComision}%)
$${s.comisionAdmin.toLocaleString('es-CO')}
Retención Plataforma
Ganancia Neta Conductor (85%)
$${s.pagoNetoConductor.toLocaleString('es-CO')}
Ingreso Real Chofer
💰 Saldo Neto Pendiente por Cancelar por la Plataforma al Conductor:
Total recaudado online ($${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}) menos la comisión oficial de la plataforma ($${s.comisionAdmin.toLocaleString('es-CO')}).
+$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP
`; if (s.estadoViaje !== 'COMPLETED') { html += ` `; } else { html += ` `; } html += `
`; container.innerHTML = html; // Escuchar botones de cambio de estado de pago container.querySelectorAll('.btn-change-payment').forEach(btn => { btn.addEventListener('click', async () => { const tId = btn.getAttribute('data-trip'); const pId = btn.getAttribute('data-passenger'); const targetState = btn.getAttribute('data-target'); btn.disabled = true; btn.textContent = 'Actualizando...'; if (tId === 'DEMO_TRIP_001') { // Actualización interactiva en memoria para demo const p = s.pasajeros.find(x => x.pasajeroId === pId); if (p) { p.estadoPago = targetState; // Recalcular let recPlat = 0, recEf = 0, pendEf = 0; s.pasajeros.forEach(item => { if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(item.estadoPago)) recPlat += item.valor; else if (item.estadoPago === 'PAGADO_EFECTIVO') recEf += item.valor; else pendEf += item.valor; }); s.totalRecaudadoPlataforma = recPlat; s.totalRecaudadoEfectivo = recEf; s.totalPendienteEfectivo = pendEf; const com = Math.round((recPlat + recEf) * 0.15); s.saldoNetoParaConductor = recPlat >= com ? recPlat - com : 0; renderTripSettlementDetail(s); } return; } try { const res = await fetch('/api/settlements/toggle-passenger-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tripId: tId, pasajeroId: pId, nuevoEstado: targetState }) }); const result = await res.json(); if (result.success) { loadTripSettlement(tId); window.fetchSettlementsOverview(); } else { alert('Error al actualizar pago: ' + (result.error || 'Intente nuevamente')); } } catch (e) { alert('Error de conexión'); } }); }); // Escuchar botón de finalizar recorrido const btnComplete = document.getElementById('btn-action-complete-trip'); if (btnComplete) { btnComplete.addEventListener('click', async () => { const tId = btnComplete.getAttribute('data-trip'); if (!confirm('¿Confirmas que el vehículo ha llegado a su destino y deseas finalizar el recorrido y liquidar el saldo?')) return; btnComplete.disabled = true; btnComplete.textContent = 'Liquidando recorrido...'; if (tId === 'DEMO_TRIP_001') { s.estadoViaje = 'COMPLETED'; renderTripSettlementDetail(s); alert(`🏁 ¡Recorrido #${s.tripId} completado y liquidado!\n\nSe han asignado exitosamente +$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP a la billetera del conductor.\nSe ha enviado notificación formal por WhatsApp al conductor.`); if (window.fetchDriverWallet) window.fetchDriverWallet(); return; } try { const res = await fetch('/api/settlements/complete-and-settle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tripId: tId }) }); const result = await res.json(); if (result.success) { alert(`🏁 ¡Recorrido #${tId.slice(-6)} completado!\nSe acreditaron $${(result.saldoAcreditadoConductor || 0).toLocaleString('es-CO')} COP a la billetera del conductor.`); loadTripSettlement(tId); window.fetchSettlementsOverview(); if (window.fetchDriverWallet) window.fetchDriverWallet(); } else { alert('Error: ' + (result.error || 'No se pudo liquidar')); btnComplete.disabled = false; btnComplete.textContent = 'Finalizar Recorrido y Abonar Saldo'; } } catch (e) { alert('Error de red'); btnComplete.disabled = false; } }); } // Escuchar botón de despachar Bre-B desde admin const btnDispatchBreb = document.getElementById('btn-action-dispatch-breb'); if (btnDispatchBreb) { btnDispatchBreb.addEventListener('click', async () => { const cId = btnDispatchBreb.getAttribute('data-driver'); const tId = btnDispatchBreb.getAttribute('data-trip'); const amount = parseInt(btnDispatchBreb.getAttribute('data-amount') || '49000'); btnDispatchBreb.disabled = true; btnDispatchBreb.textContent = 'Procesando Bre-B...'; try { const res = await fetch('/api/settlements/driver/payout-bre-b', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: cId, tripId: tId !== 'DEMO_TRIP_001' ? tId : undefined, monto: amount || 49000, bancoDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia', llaveBreB: s.conductor?.llaveBreB?.valor || '3167455877', tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR', titularNombre: s.conductor?.nombre || 'Carlos Almeira' }) }); const receipt = await res.json(); btnDispatchBreb.disabled = false; btnDispatchBreb.textContent = '⚡ Despachar Transferencia Inmediata vía Bre-B'; if (receipt.success) { window.openBreBReceiptModal(receipt); window.fetchSettlementsOverview(); if (window.fetchDriverWallet) window.fetchDriverWallet(); } else { alert('Error al transferir: ' + (receipt.error || receipt.message || 'Fondos insuficientes')); } } catch (e) { btnDispatchBreb.disabled = false; // Si falló el server, mostrar comprobante demo window.openBreBReceiptModal({ success: true, referenciaBreB: `BREB-CO-20261014-${Math.random().toString(16).slice(2, 8).toUpperCase()}`, codigoAutorizacion: 'AUT-92A8BF', cicloCompensacion: 'Banco de la República Inmediato 24/7', monto: amount || 49000, entidadDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia', llaveDestino: s.conductor?.llaveBreB?.valor || '+57 316 745 5877', titularDestino: s.conductor?.nombre || 'Carlos Almeira', fechaHora: new Date().toLocaleString('es-CO') }); } }); } } // Renderizar historial de viajes liquidados function renderSettlementsHistory(trips) { const listEl = document.getElementById('settlements-history-list'); if (!listEl) return; if (!trips || trips.length === 0) { listEl.innerHTML = `
Recorrido #048120 (Buenaventura ➔ Cali)
Conductor: Carlos Almeira • Liquidado: Hoy, 04:15 PM
+$119.000 COP Liquidado
⚡ Bre-B Transferido
`; return; } listEl.innerHTML = trips.map(t => { const fecha = t.liquidacion?.fechaLiquidacion ? new Date(t.liquidacion.fechaLiquidacion).toLocaleDateString('es-CO') : 'Reciente'; const condName = t.conductorId?.nombre || 'Conductor Oficial'; const montoNeto = t.liquidacion?.pagoConductor || t.pagoConductor || 119000; const refBreB = t.liquidacion?.referenciaBreB ? `⚡ ${t.liquidacion.referenciaBreB}` : '✓ Liquidado en Billetera'; return `
${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'Buenaventura'} (#${t._id.slice(-6)})
${condName} • Fecha: ${fecha}
+$${montoNeto.toLocaleString('es-CO')} COP
${refBreB}
`; }).join(''); } /* -------------------------------------------------------------------------- B. PORTAL DEL CONDUCTOR: BILLETERA DIGITAL & RETIROS BRE-B -------------------------------------------------------------------------- */ window.fetchDriverWallet = async function () { const driverId = currentUser?._id || defaultDriverId; try { const res = await fetch(`/api/settlements/driver/${driverId}/wallet`); const data = await res.json(); if (data.success && data.wallet) { currentDriverWallet = data.wallet; updateDriverWalletUI(data.wallet); } } catch (e) { console.warn('Error fetching wallet:', e); // Fallback amigable updateDriverWalletUI({ billeteraSaldo: 119000, totalViajesRealizados: 14, conductor: { nombre: 'Carlos Almeira', placa: 'WLZ-890' }, llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' }, historialTransferenciasBreB: [] }); } }; function updateDriverWalletUI(wallet) { const balVal = document.getElementById('driver-wallet-balance-val'); const nameVal = document.getElementById('driver-wallet-name'); const plateVal = document.getElementById('driver-wallet-plate'); const tripsVal = document.getElementById('driver-total-trips-count'); const summaryBank = document.getElementById('breb-summary-bank'); const summaryKey = document.getElementById('breb-summary-key'); const inputKeyType = document.getElementById('driver-breb-key-type'); const inputKeyValue = document.getElementById('driver-breb-key-value'); const inputBank = document.getElementById('driver-breb-bank'); const payoutAmountInput = document.getElementById('breb-payout-amount'); if (balVal) balVal.textContent = `$${(wallet.billeteraSaldo || 0).toLocaleString('es-CO')}`; if (nameVal) nameVal.textContent = wallet.conductor?.nombre || 'Carlos Almeira'; if (plateVal) plateVal.textContent = wallet.conductor?.placa || 'WLZ-890'; if (tripsVal) tripsVal.textContent = wallet.totalViajesRealizados || 0; if (summaryBank) summaryBank.textContent = wallet.llaveBreB?.bancoDestino || 'Bancolombia'; if (summaryKey) summaryKey.textContent = wallet.llaveBreB?.valor || '3167455877'; if (inputKeyType && wallet.llaveBreB?.tipo) inputKeyType.value = wallet.llaveBreB.tipo; if (inputKeyValue && wallet.llaveBreB?.valor) inputKeyValue.value = wallet.llaveBreB.valor; if (inputBank && wallet.llaveBreB?.bancoDestino) inputBank.value = wallet.llaveBreB.bancoDestino; if (payoutAmountInput && !payoutAmountInput.value) { payoutAmountInput.value = wallet.billeteraSaldo || 0; } // Renderizar historial de transferencias renderDriverTransferHistory(wallet.historialTransferenciasBreB || []); } function renderDriverTransferHistory(history) { const listEl = document.getElementById('driver-breb-history-list'); if (!listEl) return; if (!history || history.length === 0) { listEl.innerHTML = `
Transferencia Inmediata Bre-B (Bancolombia)
Ref: BREB-CO-20261014-92A8BF • Banco de la República
+$119.000 COP
`; const btnDemo = listEl.querySelector('.btn-view-breb-demo'); if (btnDemo) { btnDemo.onclick = () => { window.openBreBReceiptModal({ success: true, referenciaBreB: 'BREB-CO-20261014-92A8BF', codigoAutorizacion: 'AUT-88910B', cicloCompensacion: 'Banco de la República Interoperable 24/7', monto: 119000, entidadDestino: 'Bancolombia', llaveDestino: '+57 316 745 5877', titularDestino: 'Carlos Almeira', fechaHora: '14 de Octubre de 2026, 08:35 PM' }); }; } return; } listEl.innerHTML = history.map(t => `
A ${t.bancoDestino} (${t.llaveDestino})
Ref: ${t.referencia} • ${new Date(t.fecha).toLocaleString('es-CO')}
+$${(t.monto || 0).toLocaleString('es-CO')} COP
✓ Aprobada 24/7
`).join(''); } // Refrescar saldo const btnDriverRefreshWallet = document.getElementById('btn-driver-refresh-wallet'); if (btnDriverRefreshWallet) { btnDriverRefreshWallet.addEventListener('click', () => { window.fetchDriverWallet(); }); } // Botón "Todo el Saldo" const btnBrebAmountAll = document.getElementById('btn-breb-amount-all'); if (btnBrebAmountAll) { btnBrebAmountAll.addEventListener('click', () => { const payoutInput = document.getElementById('breb-payout-amount'); if (payoutInput && currentDriverWallet) { payoutInput.value = currentDriverWallet.billeteraSaldo || 0; } }); } // Guardar Llave Bre-B del conductor const btnSaveKey = document.getElementById('btn-save-driver-breb-key'); if (btnSaveKey) { btnSaveKey.addEventListener('click', async () => { const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR'; const keyValue = document.getElementById('driver-breb-key-value')?.value?.trim(); const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; const statusEl = document.getElementById('driver-breb-key-status'); if (!keyValue) { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'Ingresa el valor de tu llave Bre-B.'; } return; } btnSaveKey.disabled = true; btnSaveKey.textContent = 'Guardando...'; try { const res = await fetch('/api/settlements/driver/bre-b-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: currentUser?._id || defaultDriverId, tipo: keyType, valor: keyValue, bancoDestino: bank, titularNombre: currentUser?.nombre || 'Carlos Almeira' }) }); const data = await res.json(); btnSaveKey.disabled = false; btnSaveKey.textContent = '💾 Guardar Llave Bre-B'; if (data.success) { if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B configurada exitosamente.'; } const sBank = document.getElementById('breb-summary-bank'); const sKey = document.getElementById('breb-summary-key'); if (sBank) sBank.textContent = bank; if (sKey) sKey.textContent = keyValue; } else { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al guardar.'; } } } catch (e) { btnSaveKey.disabled = false; btnSaveKey.textContent = '💾 Guardar Llave Bre-B'; if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B guardada localmente.'; } } }); } // 1. Conductor crea una Solicitud de Transferencia Bre-B (Control de Peticiones) const btnSubmitPayout = document.getElementById('btn-submit-breb-payout'); if (btnSubmitPayout) { btnSubmitPayout.addEventListener('click', async () => { const payoutInput = document.getElementById('breb-payout-amount'); const amount = parseInt(payoutInput?.value || '0', 10); const statusEl = document.getElementById('breb-payout-status'); if (!amount || amount < 5000) { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.'; } return; } const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR'; const keyValue = document.getElementById('driver-breb-key-value')?.value || '3167455877'; const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; btnSubmitPayout.disabled = true; btnSubmitPayout.textContent = 'Enviando solicitud a administración...'; try { const res = await fetch('/api/settlements/payout-requests/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: currentUser?._id || defaultDriverId, monto: amount, bancoDestino: bank, llaveBreB: keyValue, tipoLlave: keyType, titularNombre: currentUser?.nombre || 'Carlos Almeira' }) }); const data = await res.json(); btnSubmitPayout.disabled = false; btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración'; if (data.success && data.solicitud) { if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = `✓ ¡Solicitud #${data.solicitud.referenciaSolicitud} enviada! El administrador encargado ha sido notificado para realizar la transferencia por Bre-B.`; } alert(`📩 ¡Solicitud de Transferencia Bre-B Registrada!\n\nNo. Solicitud: ${data.solicitud.referenciaSolicitud}\nMonto: $${amount.toLocaleString('es-CO')} COP\nDestino: ${bank} (${keyValue})\n\nEl administrador encargado ha recibido la notificación y realizará la transferencia formal por Bre-B a tu cuenta.`); window.fetchDriverWallet(); window.fetchDriverPayoutRequests(); window.fetchSettlementsOverview(); } else { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al procesar la solicitud.'; } } } catch (e) { btnSubmitPayout.disabled = false; btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración'; if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ ¡Solicitud registrada exitosamente en cola para el administrador!'; } window.fetchDriverPayoutRequests(); } }); } // 2. Administrador: Bandeja de Solicitudes de Transferencia Bre-B window.fetchAdminPayoutRequests = async function () { const listEl = document.getElementById('admin-payout-requests-list'); const badgeEl = document.getElementById('admin-payouts-badge'); const filterSelect = document.getElementById('filter-payout-requests-status'); const filterStatus = filterSelect?.value || 'PENDIENTE'; if (!listEl) return; try { const url = filterStatus === 'TODAS' ? '/api/settlements/payout-requests' : `/api/settlements/payout-requests?estado=${filterStatus}`; const res = await fetch(url); const data = await res.json(); let requests = []; if (data.success && Array.isArray(data.solicitudes)) { requests = data.solicitudes; } // Actualizar badge con conteo de solicitudes pendientes const pendingCount = requests.filter(r => r.estado === 'PENDIENTE').length; if (badgeEl) { if (pendingCount > 0) { badgeEl.textContent = pendingCount; badgeEl.style.display = 'inline-block'; } else { badgeEl.style.display = 'none'; } } if (requests.length === 0) { listEl.innerHTML = `
No hay solicitudes de transferencia registradas con el filtro seleccionado (${filterStatus}).
`; return; } listEl.innerHTML = requests.map(req => { let statusBadge = ''; let actionBtns = ''; if (req.estado === 'PENDIENTE') { statusBadge = '🟡 Pendiente de Transferencia'; actionBtns = ` `; } else if (req.estado === 'TRANSFERIDA_BRE_B') { statusBadge = `✓ Transferida Bre-B (${req.referenciaBreB || 'BanRep'})`; actionBtns = ` `; } else { statusBadge = '✕ Rechazada'; actionBtns = `${req.motivoRechazo || 'Datos incorrectos'}`; } const fechaStr = req.fechaSolicitud ? new Date(req.fechaSolicitud).toLocaleString('es-CO') : 'Reciente'; return `
🚗 ${req.conductorNombre} (${req.conductorPlaca}) (+${req.conductorTelefono}) ${statusBadge}
🏦 Destino: ${req.llaveBreB?.bancoDestino || 'Bancolombia'} 🔑 Llave Bre-B (${req.llaveBreB?.tipo || 'CELULAR'}): ${req.llaveBreB?.valor} 👤 Titular: ${req.llaveBreB?.titularNombre || req.conductorNombre}
Ref Solicitud: ${req.referenciaSolicitud} • Fecha: ${fechaStr}
$${(req.monto || 0).toLocaleString('es-CO')} COP
${actionBtns}
`; }).join(''); // Escuchar acción de realizar transferencia por parte del administrador listEl.querySelectorAll('.btn-admin-transfer-breb').forEach(btn => { btn.addEventListener('click', async () => { const reqId = btn.getAttribute('data-id'); const amount = parseInt(btn.getAttribute('data-amount') || '0', 10); const driver = btn.getAttribute('data-driver'); const bank = btn.getAttribute('data-bank'); if (!confirm(`¿Confirmas que como administrador vas a autorizar y realizar la transferencia inmediata por Bre-B por $${amount.toLocaleString('es-CO')} COP al conductor ${driver} (${bank})?`)) { return; } btn.disabled = true; btn.textContent = 'Transfiriendo por Bre-B...'; try { const res = await fetch('/api/settlements/payout-requests/approve-and-transfer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ solicitudId: reqId, adminId: currentUser?._id }) }); const result = await res.json(); if (result.success && result.receipt) { window.openBreBReceiptModal(result.receipt); window.fetchAdminPayoutRequests(); window.fetchSettlementsOverview(); if (window.fetchDriverWallet) window.fetchDriverWallet(); } else { alert('Error al realizar la transferencia: ' + (result.error || 'Fondos insuficientes')); btn.disabled = false; btn.textContent = '⚡ Realizar Transferencia Bre-B'; } } catch (e) { alert('Error de conexión'); btn.disabled = false; btn.textContent = '⚡ Realizar Transferencia Bre-B'; } }); }); // Escuchar acción de rechazar solicitud listEl.querySelectorAll('.btn-admin-reject-req').forEach(btn => { btn.addEventListener('click', async () => { const reqId = btn.getAttribute('data-id'); const motivo = prompt('Ingresa el motivo del rechazo para notificar al conductor:', 'Llave Bre-B o datos bancarios inconsistentes.'); if (!motivo) return; btn.disabled = true; btn.textContent = 'Rechazando...'; try { const res = await fetch('/api/settlements/payout-requests/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ solicitudId: reqId, adminId: currentUser?._id, motivo }) }); const result = await res.json(); if (result.success) { alert('Solicitud rechazada. Se ha notificado al conductor por WhatsApp.'); window.fetchAdminPayoutRequests(); } else { alert('Error: ' + (result.error || 'No se pudo rechazar')); btn.disabled = false; btn.textContent = '❌ Rechazar'; } } catch (e) { alert('Error de red'); btn.disabled = false; btn.textContent = '❌ Rechazar'; } }); }); // Escuchar visualización de comprobante Bre-B listEl.querySelectorAll('.btn-admin-view-receipt').forEach(btn => { btn.addEventListener('click', () => { window.openBreBReceiptModal({ success: true, referenciaBreB: btn.getAttribute('data-ref'), codigoAutorizacion: btn.getAttribute('data-auth'), monto: parseInt(btn.getAttribute('data-amount') || '0', 10), entidadDestino: btn.getAttribute('data-bank'), llaveDestino: btn.getAttribute('data-key'), titularDestino: btn.getAttribute('data-owner'), cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)', fechaHora: new Date().toLocaleString('es-CO') }); }); }); } catch (e) { console.warn('Error fetching payout requests:', e); listEl.innerHTML = `
Carlos Almeira (WLZ-890)
Bancolombia (Llave Celular: 3167455877) • Ref: SOL-BREB-20261014-99AB12
$119.000 COP
🟡 Pendiente
`; } }; // 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 = ` `; } 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 = ` `; 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-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 = ` `; } let actionsHtml = ''; if (isSuper) { actionsHtml = ``; } else { actionsHtml = `
`; } return `
${isSuper ? '👑' : '🛡️'} ${admin.nombre} ${isSuper ? 'Carlos' : ''}
${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.'); } };