// 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(); initSettlementsAndBreBSystem(); fetchTariffs(); fetchStatsAndReviews(); }); /* ========================================================================== 1. PWA & IPHONE INSTALLATION SYSTEM ========================================================================== */ function initPWA() { if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('✅ [PWA] Service Worker registrado exitosamente:', reg.scope)) .catch(err => console.warn('⚠️ [PWA Service Worker Warning]:', err)); }); } // Guardar evento de instalación de Android window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; }); const btnInstall = document.getElementById('btn-install-pwa'); const modalInstall = document.getElementById('modal-pwa-install'); const btnClose = document.getElementById('btn-close-pwa-modal'); const btnTriggerAndroid = document.getElementById('btn-trigger-android-pwa'); if (btnInstall && modalInstall) { btnInstall.addEventListener('click', () => { modalInstall.classList.add('active'); }); } if (btnClose && modalInstall) { btnClose.addEventListener('click', () => { modalInstall.classList.remove('active'); }); modalInstall.addEventListener('click', (e) => { if (e.target === modalInstall) modalInstall.classList.remove('active'); }); } if (btnTriggerAndroid) { btnTriggerAndroid.addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log('PWA Outcome:', outcome); deferredPrompt = null; if (modalInstall) modalInstall.classList.remove('active'); } else { alert('Para instalar en iPhone, sigue los 4 pasos mostrados arriba en Safari. En Android Chrome, toca los 3 puntos (⋮) y selecciona "Instalar aplicación".'); } }); } } /* ========================================================================== 2. TABS & ADMIN SUBTABS NAVIGATION ========================================================================== */ function initTabs() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); // Switcher de Vistas Principales SPA con Escudo de Seguridad window.showView = function(viewName) { const viewPublic = document.getElementById('view-public-portal'); const viewAdmin = document.getElementById('view-admin-dashboard'); const viewDriver = document.getElementById('view-driver-portal'); const adminGate = document.getElementById('admin-security-gate'); const adminContent = document.getElementById('admin-console-content'); const driverGate = document.getElementById('driver-security-gate'); const driverContent = document.getElementById('driver-console-content'); // Ocultar todas las vistas principales if (viewPublic) viewPublic.style.display = 'none'; if (viewAdmin) viewAdmin.style.display = 'none'; if (viewDriver) viewDriver.style.display = 'none'; // Deseleccionar enlaces superiores document.querySelectorAll('.nav-links a').forEach(a => a.classList.remove('active')); if (viewName === 'admin') { if (viewAdmin) viewAdmin.style.display = 'block'; const isAdmin = currentUser && (currentUser.rol === 'ADMIN' || currentUser.rol === 'OPERADOR'); if (isAdmin) { if (adminGate) adminGate.style.display = 'none'; if (adminContent) adminContent.style.display = 'block'; const adminNameEl = document.getElementById('admin-user-name'); if (adminNameEl) adminNameEl.textContent = `${currentUser.nombre} (${currentUser.rol})`; if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 250); if (window.fetchSettlementsOverview) window.fetchSettlementsOverview(); if (window.fetchAdminPayoutRequests) window.fetchAdminPayoutRequests(); } else { if (adminGate) adminGate.style.display = 'block'; if (adminContent) adminContent.style.display = 'none'; } } else if (viewName === 'driver') { if (viewDriver) viewDriver.style.display = 'block'; const isDriver = currentUser && (currentUser.rol === 'CONDUCTOR' || currentUser.rol === 'ADMIN'); if (isDriver) { if (driverGate) driverGate.style.display = 'none'; if (driverContent) driverContent.style.display = 'block'; const driverNameEl = document.getElementById('driver-active-name'); if (driverNameEl) driverNameEl.textContent = currentUser.nombre; if (window.fetchDriverWallet) window.fetchDriverWallet(); if (window.fetchDriverPayoutRequests) window.fetchDriverPayoutRequests(); } else { if (driverGate) driverGate.style.display = 'block'; if (driverContent) driverContent.style.display = 'none'; } } else { // Default: Portal Público limpio if (viewPublic) viewPublic.style.display = 'block'; const linkReservas = document.getElementById('nav-link-reservas'); if (linkReservas) linkReservas.classList.add('active'); } }; // Selector elegante de servicios públicos: Pasajeros vs Encomiendas window.activatePublicService = function(targetTabId) { const btnPasajeros = document.getElementById('btn-service-pasajeros'); const btnFletes = document.getElementById('btn-service-fletes'); const tabPasajeros = document.getElementById('tab-pasajeros'); const tabFletes = document.getElementById('tab-fletes'); if (targetTabId === 'tab-fletes') { if (btnPasajeros) btnPasajeros.classList.remove('active'); if (btnFletes) btnFletes.classList.add('active'); if (tabPasajeros) tabPasajeros.classList.remove('active'); if (tabFletes) tabFletes.classList.add('active'); } else { if (btnPasajeros) btnPasajeros.classList.add('active'); if (btnFletes) btnFletes.classList.remove('active'); if (tabPasajeros) tabPasajeros.classList.add('active'); if (tabFletes) tabFletes.classList.remove('active'); } }; const btnServicePasajeros = document.getElementById('btn-service-pasajeros'); const btnServiceFletes = document.getElementById('btn-service-fletes'); if (btnServicePasajeros) { btnServicePasajeros.addEventListener('click', () => window.activatePublicService('tab-pasajeros')); } if (btnServiceFletes) { btnServiceFletes.addEventListener('click', () => window.activatePublicService('tab-fletes')); } // Enlaces públicos del navbar y hero const navLinkReservas = document.getElementById('nav-link-reservas'); if (navLinkReservas) { navLinkReservas.addEventListener('click', (e) => { e.preventDefault(); window.showView('public'); window.activatePublicService('tab-pasajeros'); document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' }); }); } const navLinkFletes = document.getElementById('nav-link-fletes'); const heroBtnFlete = document.getElementById('hero-btn-flete'); [navLinkFletes, heroBtnFlete].forEach(el => { if (el) { el.addEventListener('click', (e) => { e.preventDefault(); window.showView('public'); window.activatePublicService('tab-fletes'); document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' }); }); } }); const navLinkReviews = document.getElementById('nav-link-reviews'); if (navLinkReviews) { navLinkReviews.addEventListener('click', (e) => { e.preventDefault(); window.showView('public'); document.getElementById('estadisticas')?.scrollIntoView({ behavior: 'smooth' }); }); } // Botones de acceso exclusivo para Roles en Navbar const navBtnOpenAdmin = document.getElementById('nav-btn-open-admin'); if (navBtnOpenAdmin) { navBtnOpenAdmin.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = '#admin'; window.showView('admin'); }); } const navBtnOpenDriver = document.getElementById('nav-btn-open-driver'); if (navBtnOpenDriver) { navBtnOpenDriver.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = '#conductor'; window.showView('driver'); }); } // Botones de regreso al sitio público const btnAdminViewPublic = document.getElementById('btn-admin-view-public'); const btnDriverViewPublic = document.getElementById('btn-driver-view-public'); const btnGateBackPublic = document.getElementById('btn-gate-back-public'); const btnGateDriverBack = document.getElementById('btn-gate-driver-back-public'); [btnAdminViewPublic, btnDriverViewPublic, btnGateBackPublic, btnGateDriverBack].forEach(b => { if (b) { b.addEventListener('click', () => { window.location.hash = '#reservas'; window.showView('public'); }); } }); // Botones para abrir modal de login desde los escudos de seguridad const btnGateAdminLogin = document.getElementById('btn-gate-admin-login'); const btnGateDriverLogin = document.getElementById('btn-gate-driver-login'); const authModal = document.getElementById('modal-auth-dripzone'); [btnGateAdminLogin, btnGateDriverLogin].forEach(b => { if (b && authModal) { b.addEventListener('click', () => authModal.classList.add('active')); } }); const btnAdminLogout = document.getElementById('btn-admin-logout'); if (btnAdminLogout) { btnAdminLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); currentUser = null; if (window.updateUserUI) window.updateUserUI(null); window.location.hash = '#reservas'; window.showView('public'); }); } // Compatibilidad genérica para pestañas window.activateTab = function(targetId) { tabBtns.forEach(b => { if (b.getAttribute('data-tab') === targetId) { b.classList.add('active'); } else { b.classList.remove('active'); } }); tabContents.forEach(c => { if (c.id === targetId) { c.classList.add('active'); } else { c.classList.remove('active'); } }); if (targetId === 'tab-admin' && leafletMap) { setTimeout(() => { leafletMap.invalidateSize(); }, 250); } }; tabBtns.forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-tab'); window.activateTab(targetId); }); }); // Sub-tabs de la sección administrativa const subRadarBtn = document.getElementById('btn-admin-subtab-radar'); const subTarifasBtn = document.getElementById('btn-admin-subtab-tarifas'); const subLiquidacionesBtn = document.getElementById('btn-admin-subtab-liquidaciones'); const viewRadar = document.getElementById('admin-view-radar'); const viewTarifas = document.getElementById('admin-view-tarifas'); const viewLiquidaciones = document.getElementById('admin-view-liquidaciones'); function setAdminSubTab(activeBtn, activeView) { [subRadarBtn, subTarifasBtn, subLiquidacionesBtn].forEach(b => b?.classList.remove('active')); [viewRadar, viewTarifas, viewLiquidaciones].forEach(v => { if (v) v.style.display = 'none'; }); if (activeBtn) activeBtn.classList.add('active'); if (activeView) activeView.style.display = 'block'; } if (subRadarBtn && viewRadar) { subRadarBtn.addEventListener('click', () => { setAdminSubTab(subRadarBtn, viewRadar); if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 200); }); } if (subTarifasBtn && viewTarifas) { subTarifasBtn.addEventListener('click', () => { setAdminSubTab(subTarifasBtn, viewTarifas); }); } if (subLiquidacionesBtn && viewLiquidaciones) { subLiquidacionesBtn.addEventListener('click', () => { setAdminSubTab(subLiquidacionesBtn, viewLiquidaciones); if (window.fetchSettlementsOverview) window.fetchSettlementsOverview(); }); } // Sub-tabs de la sección del conductor const subDriverWalletBtn = document.getElementById('btn-driver-subtab-billetera'); const subDriverRegistroBtn = document.getElementById('btn-driver-subtab-registro'); const viewDriverWallet = document.getElementById('driver-view-billetera'); const viewDriverRegistro = document.getElementById('driver-view-registro'); if (subDriverWalletBtn && subDriverRegistroBtn && viewDriverWallet && viewDriverRegistro) { subDriverWalletBtn.addEventListener('click', () => { subDriverWalletBtn.classList.add('active'); subDriverRegistroBtn.classList.remove('active'); viewDriverWallet.style.display = 'block'; viewDriverRegistro.style.display = 'none'; if (window.fetchDriverWallet) window.fetchDriverWallet(); }); subDriverRegistroBtn.addEventListener('click', () => { subDriverRegistroBtn.classList.add('active'); subDriverWalletBtn.classList.remove('active'); viewDriverWallet.style.display = 'none'; viewDriverRegistro.style.display = 'block'; }); } // Enrutamiento por Hash en la URL (#admin, #conductor, etc.) function handleUrlHash() { const hash = (window.location.hash || '').toLowerCase(); if (hash === '#admin') { window.showView('admin'); } else if (hash === '#conductor' || hash === '#driver') { window.showView('driver'); } else { window.showView('public'); if (hash === '#fletes') { window.activatePublicService('tab-fletes'); } } } window.addEventListener('hashchange', handleUrlHash); handleUrlHash(); } /* ========================================================================== 3. AUTHENTICATION (DRIPZONE STYLE CON OTP WHATSAPP + EMAIL) ========================================================================== */ function initAuthSystem() { const modal = document.getElementById('modal-auth-dripzone'); const btnOpen = document.getElementById('btn-open-auth-modal'); const btnClose = document.getElementById('btn-close-auth-modal'); const btnLogout = document.getElementById('btn-logout'); const modeLoginBtn = document.getElementById('btn-auth-mode-login'); const modeRegisterBtn = document.getElementById('btn-auth-mode-register'); const groupName = document.getElementById('group-auth-name'); const authModalTitle = document.getElementById('auth-modal-title'); const step1 = document.getElementById('auth-step-1'); const step2 = document.getElementById('auth-step-2'); const formStep1 = document.getElementById('form-auth-request-otp'); const btnVerifyOtp = document.getElementById('btn-submit-verify-otp'); const btnResend = document.getElementById('btn-auth-resend-otp'); const reqError = document.getElementById('auth-request-error'); const verError = document.getElementById('auth-verify-error'); const targetLabel = document.getElementById('auth-sent-target'); let authMode = 'login'; // 'login' | 'register' let currentIdentifier = ''; let currentEmail = ''; // Abrir y cerrar modal if (btnOpen && modal) { btnOpen.addEventListener('click', () => { reqError.style.display = 'none'; verError.style.display = 'none'; step1.style.display = 'block'; step2.style.display = 'none'; modal.classList.add('active'); }); } if (btnClose && modal) { btnClose.addEventListener('click', () => modal.classList.remove('active')); modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('active'); }); } // Alternar entre Login y Registro if (modeLoginBtn && modeRegisterBtn) { modeLoginBtn.addEventListener('click', () => { authMode = 'login'; modeLoginBtn.classList.add('active'); modeRegisterBtn.classList.remove('active'); if (groupName) groupName.style.display = 'none'; if (authModalTitle) authModalTitle.textContent = 'Acceso Seguro con Verificación OTP'; }); modeRegisterBtn.addEventListener('click', () => { authMode = 'register'; modeRegisterBtn.classList.add('active'); modeLoginBtn.classList.remove('active'); if (groupName) groupName.style.display = 'block'; if (authModalTitle) authModalTitle.textContent = 'Registro de Usuario & Validación 2FA'; }); } // Paso 1: Enviar OTP por WhatsApp if (formStep1) { formStep1.addEventListener('submit', async (e) => { e.preventDefault(); const phoneInput = document.getElementById('auth-input-phone'); const emailInput = document.getElementById('auth-input-email'); const nameInput = document.getElementById('auth-input-name'); const btnSubmit = document.getElementById('btn-submit-send-otp'); const phone = phoneInput?.value.trim() || ''; const email = emailInput?.value.trim() || ''; const name = nameInput?.value.trim() || ''; if (!phone) { showError(reqError, 'Ingresa tu número de WhatsApp.'); return; } currentIdentifier = phone; currentEmail = email; btnSubmit.disabled = true; btnSubmit.innerHTML = '⏳ Enviando código de seguridad...'; reqError.style.display = 'none'; try { const res = await fetch('/api/auth/send-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: phone, email }) }); const data = await res.json(); if (res.ok && data.success) { step1.style.display = 'none'; step2.style.display = 'block'; if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${phone}`; // Si el servidor devolvió preview (entorno local sin SIM de Evolution), mostrar aviso if (data.codePreview) { console.log(`[Demo Code OTP]: ${data.codePreview}`); verError.style.display = 'block'; verError.style.color = '#38bdf8'; verError.innerHTML = `ℹ️ Código de prueba generado: ${data.codePreview}`; } focusFirstOtp(); } else { showError(reqError, data.error || 'No se pudo enviar el código OTP.'); } } catch (err) { showError(reqError, 'Error de conexión. Inténtalo de nuevo.'); } finally { btnSubmit.disabled = false; btnSubmit.innerHTML = '📲 Enviar Código de Verificación OTP'; } }); } // Cajas OTP con avance automático y soporte de pegado const otpInputs = [ document.getElementById('otp-1'), document.getElementById('otp-2'), document.getElementById('otp-3'), document.getElementById('otp-4'), document.getElementById('otp-5'), document.getElementById('otp-6') ]; otpInputs.forEach((input, index) => { if (!input) return; input.addEventListener('input', (e) => { const val = e.target.value; if (val.length === 1 && index < 5) { otpInputs[index + 1].focus(); } checkAutoSubmitOtp(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !input.value && index > 0) { otpInputs[index - 1].focus(); } }); input.addEventListener('paste', (e) => { e.preventDefault(); const pasteData = e.clipboardData.getData('text').trim(); if (/^\d{6}$/.test(pasteData)) { pasteData.split('').forEach((digit, i) => { if (otpInputs[i]) otpInputs[i].value = digit; }); checkAutoSubmitOtp(); } }); }); function focusFirstOtp() { otpInputs.forEach(i => { if (i) i.value = ''; }); if (otpInputs[0]) otpInputs[0].focus(); } function getEnteredOtp() { return otpInputs.map(i => i?.value || '').join(''); } function checkAutoSubmitOtp() { const code = getEnteredOtp(); if (code.length === 6) { submitVerifyOtp(code); } } // Paso 2: Verificar OTP if (btnVerifyOtp) { btnVerifyOtp.addEventListener('click', () => { const code = getEnteredOtp(); if (code.length < 6) { showError(verError, 'Ingresa los 6 dígitos del código.'); return; } submitVerifyOtp(code); }); } async function submitVerifyOtp(code) { if (!btnVerifyOtp) return; btnVerifyOtp.disabled = true; btnVerifyOtp.innerHTML = '⏳ Verificando código...'; verError.style.display = 'none'; const nameInput = document.getElementById('auth-input-name'); const name = nameInput?.value.trim() || ''; try { const res = await fetch('/api/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: currentIdentifier, code, nombre: name, email: currentEmail }) }); const data = await res.json(); if (res.ok && data.success) { localStorage.setItem('pt_token', data.token); currentUser = data.user; updateUserUI(currentUser); modal.classList.remove('active'); alert(`¡Bienvenido a Pacifictrans Go, ${currentUser.nombre}! Tu identidad ha sido verificada con éxito.`); } else { showError(verError, data.error || 'Código incorrecto. Verifica los 6 dígitos.'); } } catch (err) { showError(verError, 'Error al validar el código OTP.'); } finally { btnVerifyOtp.disabled = false; btnVerifyOtp.innerHTML = '✅ Validar y Acceder'; } } // Reenviar OTP if (btnResend) { btnResend.addEventListener('click', () => { formStep1.dispatchEvent(new Event('submit')); }); } // Selector Rápido de Roles para Auditoría y Pruebas document.querySelectorAll('.btn-role-quick-login').forEach(btn => { btn.addEventListener('click', async () => { const role = btn.getAttribute('data-role') || 'ADMIN'; btn.disabled = true; const originalText = btn.innerHTML; btn.innerHTML = '⏳ Accediendo...'; try { const res = await fetch('/api/auth/quick-role-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }) }); const data = await res.json(); if (data.success && data.user) { localStorage.setItem('pt_token', data.token); currentUser = data.user; updateUserUI(currentUser); if (modal) modal.classList.remove('active'); if (role === 'ADMIN') { window.location.hash = '#admin'; window.showView('admin'); } else if (role === 'CONDUCTOR') { window.location.hash = '#conductor'; window.showView('driver'); } else { window.location.hash = '#reservas'; window.showView('public'); } } else { alert('Error de acceso rápido: ' + (data.error || 'Intenta de nuevo')); } } catch (e) { console.error('Quick role error:', e); alert('Error de conexión al servidor'); } finally { btn.disabled = false; btn.innerHTML = originalText; } }); }); // Cerrar sesión if (btnLogout) { btnLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); currentUser = null; updateUserUI(null); window.location.hash = '#reservas'; window.showView('public'); }); } // Validar sesión persistida checkPersistedSession(); } async function checkPersistedSession() { const token = localStorage.getItem('pt_token'); if (!token) return; try { const res = await fetch(`/api/auth/me?token=${token}`); if (res.ok) { const data = await res.json(); if (data.user) { currentUser = data.user; updateUserUI(currentUser); } } else { localStorage.removeItem('pt_token'); } } catch (err) { console.warn('⚠️ [Session Verify Error]:', err); } } function updateUserUI(user) { const btnAuthOpen = document.getElementById('btn-open-auth-modal'); const userBadge = document.getElementById('user-profile-badge'); const userNameEl = document.getElementById('user-display-name'); const userRoleBadge = document.getElementById('user-role-badge'); const navBtnAdmin = document.getElementById('nav-btn-open-admin'); const navBtnDriver = document.getElementById('nav-btn-open-driver'); const inputUserName = document.getElementById('user-name'); const inputUserPhone = document.getElementById('user-phone'); const inputSenderPhone = document.getElementById('sender-phone'); if (user) { if (btnAuthOpen) btnAuthOpen.style.display = 'none'; if (userBadge) userBadge.style.display = 'inline-flex'; if (userNameEl) userNameEl.textContent = user.nombre.split(' ')[0]; if (userRoleBadge) userRoleBadge.textContent = `✅ ${user.rol}`; // Mostrar accesos a consolas según rol del usuario if (navBtnAdmin) { navBtnAdmin.style.display = (user.rol === 'ADMIN' || user.rol === 'OPERADOR') ? 'inline-flex' : 'none'; } if (navBtnDriver) { navBtnDriver.style.display = (user.rol === 'CONDUCTOR' || user.rol === 'ADMIN') ? 'inline-flex' : 'none'; } // Autofill en formularios de la plataforma if (inputUserName && !inputUserName.value) inputUserName.value = user.nombre; if (inputUserPhone && !inputUserPhone.value) inputUserPhone.value = user.telefono; if (inputSenderPhone && !inputSenderPhone.value) inputSenderPhone.value = user.telefono; // 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 const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`; const totalPriceEl = document.querySelector('.total-price'); if (totalPriceEl) totalPriceEl.textContent = formattedPasajero; const btnBook = document.getElementById('btn-book-pool'); if (btnBook && !btnBook.disabled) { btnBook.innerHTML = ` Confirmar y Pagar Cupo (${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 bookBtn = document.getElementById('btn-book-pool'); const poolDots = document.querySelectorAll('.seat-dot'); let currentSeatsFilled = 3; 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(); } if (bookBtn) { bookBtn.addEventListener('click', async (e) => { e.preventDefault(); // Verificación de seguridad: si no ha validado identidad con OTP, pedir autenticación if (!currentUser) { 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; } bookBtn.disabled = true; bookBtn.innerHTML = ` Procesando Reserva... `; setTimeout(() => { currentSeatsFilled = 4; updateSeatVisuals(currentSeatsFilled); const alertBox = document.getElementById('pool-dispatch-alert'); if (alertBox) { alertBox.style.display = 'block'; alertBox.scrollIntoView({ behavior: 'smooth' }); } bookBtn.disabled = false; const formatted = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`; bookBtn.innerHTML = `✅ Cupo Reservado Exitosamente (${formatted})`; }, 1200); }); } 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`; } 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 KM_DIST = 115; // Corredor Cali - Buenaventura 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 total = Math.round(subtotal + seguro); 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 (totalPriceEl) totalPriceEl.textContent = `$${total.toLocaleString('es-CO')} COP`; }; if (packageTypeSelect) packageTypeSelect.addEventListener('change', window.recalculateFreight); if (declaredValueInput) declaredValueInput.addEventListener('input', window.recalculateFreight); window.recalculateFreight(); } /* ========================================================================== 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})
📍 Recogida: ${p.direccionRecogida}
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(); }