// Pacific Trans GO - Interactive Web Platform & Native Android Integration Logic /* ========================================================================== PACIFICTRANS GO - NATIVE ANDROID BRIDGE & AUTONOMOUS API INTERCEPTOR ========================================================================== */ const API_REMOTE_HOST = 'https://pacifictrans.almeira.com.co'; const isCapacitorNative = () => { return ( (typeof window !== 'undefined' && window.Capacitor && (window.Capacitor.isNativePlatform?.() || window.Capacitor.getPlatform?.() === 'android')) || (typeof window !== 'undefined' && (window.location.protocol === 'file:' || window.location.origin.includes('localhost') || window.location.origin.includes('capacitor://'))) ); }; // Interceptor global para que toda llamada fetch('/api/...') use el host remoto automáticamente en la App Nativa if (typeof window !== 'undefined' && window.fetch) { const originalFetch = window.fetch; window.fetch = function (resource, init) { if (typeof resource === 'string' && resource.startsWith('/api/')) { if (isCapacitorNative()) { resource = API_REMOTE_HOST + resource; } } return originalFetch.call(this, resource, init); }; } let currentTariffs = { tarifaPasajeroFija: 80000, precioBasePaquete: 25000, precioKmPaquete: 600, factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 }, seguroBasePaquete: 5000, porcentajeComisionAdmin: 15, costoPenalidadCancelacion: 15000, radioMaxPoolingMetros: 4000, numeroAtencionWhatsApp: '573167455877', nombreSoporte: 'Atención Pacific Trans GO' }; let currentUser = null; let leafletMap = null; let deferredPrompt = null; // Drivers Flota con coordenadas reales en el corredor Cali - Buenaventura const activeDrivers = [ { id: '1', nombre: 'Mario Fernando G.', vehiculo: 'Renault Duster (WLZ-890)', barrio: 'Granada (Comuna 2)', distCali: '1.2 km', lat: 3.4552, lng: -76.5350, estado: 'DISPONIBLE' }, { id: '2', nombre: 'Hernán Darío P.', vehiculo: 'Toyota Fortuner (VBF-312)', barrio: 'San Antonio (Comuna 3)', distCali: '2.4 km', lat: 3.4485, lng: -76.5415, estado: 'ASIGNADO' }, { id: '3', nombre: 'Mauricio V.', vehiculo: 'Chevrolet Tracker (HJK-456)', barrio: 'El Ingenio (Comuna 17)', distCali: '6.8 km', lat: 3.3850, lng: -76.5290, estado: 'DISPONIBLE' }, { id: '4', nombre: 'Javier Restrepo', vehiculo: 'Nissan Kicks (RTY-789)', barrio: 'Limonar (Comuna 19)', distCali: '5.1 km', lat: 3.6580, lng: -76.6900, estado: 'EN_RUTA' } ]; document.addEventListener('DOMContentLoaded', () => { initNativeBridge(); initPWA(); initTabs(); initAuthSystem(); initNotificationsSystem(); initPassengerCalculator(); initFreightCalculator(); initLeafletRadarMap(); initReviewsAndStats(); initTariffsManager(); initCouponsManager(); initSettlementsAndBreBSystem(); initUserProfileManager(); initAdministratorsManager(); fetchTariffs(); fetchAdminCoupons(); fetchStatsAndReviews(); }); /* ========================================================================== NATIVE CAPACITOR ANDROID INTEGRATION MODULE ========================================================================== */ function initNativeBridge() { initNativeSystemBars(); initNativeBackButton(); initNativeGPS(); initNativeHaptics(); initNativeNetworkMonitor(); applyNativeUIPolish(); } // 1. Barras de Estado & Splash Screen Nativo async function initNativeSystemBars() { if (window.Capacitor?.isPluginAvailable?.('StatusBar')) { try { const { StatusBar, Style } = window.Capacitor.Plugins; await StatusBar.setStyle({ style: Style.Dark }); await StatusBar.setBackgroundColor({ color: '#080c14' }); await StatusBar.setOverlaysWebView({ overlay: false }); } catch (e) { console.warn('[StatusBar Warning]:', e); } } if (window.Capacitor?.isPluginAvailable?.('SplashScreen')) { try { const { SplashScreen } = window.Capacitor.Plugins; setTimeout(() => { SplashScreen.hide({ fadeOutDuration: 400 }); }, 700); } catch (e) { console.warn('[SplashScreen Warning]:', e); } } } // 2. Control Inteligente del Botón Físico 'Atrás' en Android function initNativeBackButton() { if (!window.Capacitor?.isPluginAvailable?.('App')) return; const { App } = window.Capacitor.Plugins; let lastBackTime = 0; App.addListener('backButton', () => { // 1. Cerrar modales abiertos o dropdown de notificaciones const activeModal = document.querySelector('.pwa-modal-overlay.active, #notif-dropdown-panel.show'); if (activeModal) { activeModal.classList.remove('active', 'show'); triggerHaptic('light'); return; } // 2. Si se encuentra en el Dashboard Administrativo o Portal de Conductor, regresar a Vista Pública const viewAdmin = document.getElementById('view-admin-dashboard'); const viewDriver = document.getElementById('view-driver-portal'); if (viewAdmin && viewAdmin.style.display !== 'none') { if (typeof window.showView === 'function') window.showView('public'); triggerHaptic('light'); return; } if (viewDriver && viewDriver.style.display !== 'none') { if (typeof window.showView === 'function') window.showView('public'); triggerHaptic('light'); return; } // 3. Vista principal: Doble pulsación para salir de la App const now = Date.now(); if (now - lastBackTime < 2200) { App.exitApp(); } else { lastBackTime = now; showNativeToast('Presiona atrás nuevamente para salir'); triggerHaptic('light'); } }); } // Variables globales de coordenadas GPS detectadas window.passengerGpsCoords = [-76.5320, 3.4215]; window.freightPickupCoords = [-76.5320, 3.4215]; window.freightDeliveryCoords = [-77.0250, 3.8820]; // Helper Centralizado: Obtiene coordenadas GPS y geocodifica a dirección humana real async function getCurrentUserLocation(buttonEl = null) { let originalText = ''; if (buttonEl) { triggerHaptic('medium'); originalText = buttonEl.innerHTML; buttonEl.disabled = true; buttonEl.innerHTML = '⏳ Obteniendo GPS...'; } try { let coords = null; if (window.Capacitor?.isPluginAvailable?.('Geolocation')) { const { Geolocation } = window.Capacitor.Plugins; try { const check = await Geolocation.checkPermissions(); if (check.location !== 'granted') { const req = await Geolocation.requestPermissions(); if (req.location !== 'granted') { showNativeToast('⚠️ Permiso de ubicación requerido para GPS'); return null; } } } catch (pErr) { } const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true, timeout: 12000 }); coords = { lat: pos.coords.latitude, lng: pos.coords.longitude }; } else if (navigator.geolocation) { coords = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( p => resolve({ lat: p.coords.latitude, lng: p.coords.longitude }), err => reject(err), { enableHighAccuracy: true, timeout: 12000 } ); }); } if (!coords) { showNativeToast('⚠️ No se pudo obtener la señal GPS'); return null; } if (buttonEl) buttonEl.innerHTML = '🔍 Identificando dirección...'; // Geocodificación inversa satelital a través del backend let address = `Ubicación GPS (${coords.lat.toFixed(5)}, ${coords.lng.toFixed(5)})`; let city = (coords.lat > 3.6 && coords.lng < -76.8) ? 'Buenaventura' : 'Cali'; try { const res = await fetch(`/api/geo/reverse?lat=${coords.lat}&lng=${coords.lng}`); if (res.ok) { const data = await res.json(); if (data.success && data.address) { address = data.address; city = data.city || city; } } } catch (geoErr) { console.warn('⚠️ [Reverse geocode notice]:', geoErr); } triggerHaptic('success'); return { lat: coords.lat, lng: coords.lng, address, city }; } catch (err) { console.warn('[GPS Location Error]:', err); showNativeToast('⚠️ No se pudo obtener la ubicación actual'); triggerHaptic('warning'); return null; } finally { if (buttonEl) { buttonEl.disabled = false; buttonEl.innerHTML = originalText; } } } // 3. Geolocalización GPS Nativa de Alta Precisión para Pasajeros function initNativeGPS() { const btnGps = document.getElementById('btn-native-gps-cali'); if (!btnGps) return; btnGps.addEventListener('click', async () => { const loc = await getCurrentUserLocation(btnGps); if (loc) { window.passengerGpsCoords = [loc.lng, loc.lat]; // Centrar mapa de radar y posicionar marcador del pasajero if (leafletMap && typeof L !== 'undefined') { leafletMap.setView([loc.lat, loc.lng], 16); const userIcon = L.divIcon({ className: 'user-live-gps-pin', html: '
📍
', iconSize: [36, 36], iconAnchor: [18, 18] }); L.marker([loc.lat, loc.lng], { icon: userIcon }) .addTo(leafletMap) .bindPopup(`📍 Tu Ubicación Actual
${loc.address}`) .openPopup(); } const addrInput = document.getElementById('cali-address'); if (addrInput) { addrInput.value = loc.address; } showNativeToast(`📍 Dirección detectada: ${loc.address}`); } }); } // 4. Feedback Háptico Táctil function triggerHaptic(type = 'light') { if (!window.Capacitor?.isPluginAvailable?.('Haptics')) return; const { Haptics, ImpactStyle, NotificationType } = window.Capacitor.Plugins; try { if (type === 'medium') Haptics.impact({ style: ImpactStyle.Medium }); else if (type === 'heavy') Haptics.impact({ style: ImpactStyle.Heavy }); else if (type === 'success') Haptics.notification({ type: NotificationType.Success }); else if (type === 'warning') Haptics.notification({ type: NotificationType.Warning }); else Haptics.impact({ style: ImpactStyle.Light }); } catch (e) { } } function initNativeHaptics() { document.addEventListener('click', (e) => { const target = e.target.closest('button, .chip-payment, .btn-qty-seat, .service-pill-btn, .rating-star'); if (target) { triggerHaptic('light'); } }); } // 5. Monitor de Red y Detección de Desconexión en Carretera function initNativeNetworkMonitor() { const banner = document.getElementById('native-offline-banner'); const updateStatus = (connected) => { if (!connected) { if (banner) banner.style.display = 'block'; triggerHaptic('warning'); } else { if (banner) banner.style.display = 'none'; if (typeof fetchTariffs === 'function') fetchTariffs(); } }; if (window.Capacitor?.isPluginAvailable?.('Network')) { const { Network } = window.Capacitor.Plugins; Network.getStatus().then(status => updateStatus(status.connected)); Network.addListener('networkStatusChange', status => updateStatus(status.connected)); } else { window.addEventListener('online', () => updateStatus(true)); window.addEventListener('offline', () => updateStatus(false)); if (!navigator.onLine) updateStatus(false); } } // 6. Notificaciones Toast Nativas function showNativeToast(message, duration = 2400) { const existing = document.querySelector('.native-toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = 'native-toast'; toast.innerHTML = message; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s ease'; setTimeout(() => toast.remove(), 300); }, duration); } // 7. Adaptaciones Visuales en Modo Nativo function applyNativeUIPolish() { if (isCapacitorNative()) { const navApp = document.getElementById('nav-link-app'); if (navApp) navApp.style.display = 'none'; const downloadSection = document.getElementById('descarga-apk'); if (downloadSection) { downloadSection.innerHTML = `
⚡ MODO NATIVO ANDROID ACTIVO

Pacific Trans GO Nativa

Ejecutando en hardware acelerado con soporte GPS satelital de alta precisión, hápticos y respaldo offline local.

`; } } } /* ========================================================================== 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 isAuthorized = currentUser && (currentUser.rol === 'ADMIN' || currentUser.rol === 'OPERADOR' || currentUser.esSuperAdmin); if (isAuthorized) { 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})`; } // Aplicar filtrado dinámico de menú según rol (Superadmin/Admin vs Operador) if (typeof window.applyAdminRoleMenuPermissions === 'function') { window.applyAdminRoleMenuPermissions(currentUser); } // Activar la pestaña administrativa activa (por defecto Radar) if (typeof window.setAdminSubTab === 'function') { window.setAdminSubTab(window.activeAdminSubTab || 'radar'); } if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 250); } 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' || currentUser.esSuperAdmin); if (isDriver) { if (driverGate) driverGate.style.display = 'none'; if (driverContent) driverContent.style.display = 'block'; const driverTopbarName = document.getElementById('driver-topbar-name') || document.getElementById('driver-active-name'); if (driverTopbarName) { const placa = currentUser.vehiculoPlaca ? ` (${currentUser.vehiculoPlaca})` : ''; driverTopbarName.textContent = `${currentUser.nombre}${placa}`; } const driverWalletName = document.getElementById('driver-wallet-name'); if (driverWalletName) driverWalletName.textContent = currentUser.nombre; const driverWalletPlate = document.getElementById('driver-wallet-plate'); if (driverWalletPlate && currentUser.vehiculoPlaca) driverWalletPlate.textContent = currentUser.vehiculoPlaca; if (typeof window.setDriverSubTab === 'function') { window.setDriverSubTab(window.activeDriverSubTab || 'billetera'); } } 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'); } if (currentUser && typeof window.autofillFormsWithUserData === 'function') { window.autofillFormsWithUserData(currentUser); } }; const btnServicePasajeros = document.getElementById('btn-service-pasajeros'); const btnServiceFletes = document.getElementById('btn-service-fletes'); if (btnServicePasajeros) { btnServicePasajeros.addEventListener('click', () => window.activatePublicService('tab-pasajeros')); } if (btnServiceFletes) { btnServiceFletes.addEventListener('click', () => window.activatePublicService('tab-fletes')); } // Enlaces públicos del navbar y hero const navLinkReservas = document.getElementById('nav-link-reservas'); if (navLinkReservas) { navLinkReservas.addEventListener('click', (e) => { e.preventDefault(); window.showView('public'); window.activatePublicService('tab-pasajeros'); document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' }); }); } const navLinkFletes = document.getElementById('nav-link-fletes'); const heroBtnFlete = document.getElementById('hero-btn-flete'); [navLinkFletes, heroBtnFlete].forEach(el => { if (el) { el.addEventListener('click', (e) => { e.preventDefault(); window.showView('public'); window.activatePublicService('tab-fletes'); document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' }); }); } }); const navLinkReviews = document.getElementById('nav-link-reviews'); if (navLinkReviews) { navLinkReviews.addEventListener('click', (e) => { e.preventDefault(); window.showView('public'); document.getElementById('estadisticas')?.scrollIntoView({ behavior: 'smooth' }); }); } // Botones de acceso exclusivo para Roles en Navbar const navBtnOpenAdmin = document.getElementById('nav-btn-open-admin'); if (navBtnOpenAdmin) { navBtnOpenAdmin.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = '#admin'; window.showView('admin'); }); } const navBtnOpenDriver = document.getElementById('nav-btn-open-driver'); if (navBtnOpenDriver) { navBtnOpenDriver.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = '#conductor'; window.showView('driver'); }); } // Botones de regreso al sitio público const btnAdminViewPublic = document.getElementById('btn-admin-view-public'); const btnDriverViewPublic = document.getElementById('btn-driver-view-public'); const btnGateBackPublic = document.getElementById('btn-gate-back-public'); const btnGateDriverBack = document.getElementById('btn-gate-driver-back-public'); [btnAdminViewPublic, btnDriverViewPublic, btnGateBackPublic, btnGateDriverBack].forEach(b => { if (b) { b.addEventListener('click', () => { window.location.hash = '#reservas'; window.showView('public'); }); } }); // Botones para abrir modal de login desde los escudos de seguridad const btnGateAdminLogin = document.getElementById('btn-gate-admin-login'); const btnGateDriverLogin = document.getElementById('btn-gate-driver-login'); const authModal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone'); [btnGateAdminLogin, btnGateDriverLogin].forEach(b => { if (b && authModal) { b.addEventListener('click', () => authModal.classList.add('active')); } }); const btnAdminLogout = document.getElementById('btn-admin-logout'); if (btnAdminLogout) { btnAdminLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); localStorage.removeItem('pt_token_expires_at'); currentUser = null; if (window.updateUserUI) window.updateUserUI(null); window.location.hash = '#reservas'; window.showView('public'); }); } // Compatibilidad genérica para pestañas window.activateTab = function (targetId) { tabBtns.forEach(b => { if (b.getAttribute('data-tab') === targetId) { b.classList.add('active'); } else { b.classList.remove('active'); } }); tabContents.forEach(c => { if (c.id === targetId) { c.classList.add('active'); } else { c.classList.remove('active'); } }); if (targetId === 'tab-admin' && leafletMap) { setTimeout(() => { leafletMap.invalidateSize(); }, 250); } }; tabBtns.forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-tab'); window.activateTab(targetId); }); }); // ========================================================================== // BARRA DE MENÚ DE NAVEGACIÓN ADMINISTRATIVA POR ROLES // ========================================================================== window.activeAdminSubTab = 'radar'; window.activeDriverSubTab = 'billetera'; window.applyAdminRoleMenuPermissions = function (user) { if (!user) return; const isOperador = user.rol === 'OPERADOR'; const btnRadar = document.getElementById('btn-admin-subtab-radar'); const btnLiquidaciones = document.getElementById('btn-admin-subtab-liquidaciones'); const btnTarifas = document.getElementById('btn-admin-subtab-tarifas'); const btnCupones = document.getElementById('btn-admin-subtab-cupones'); const btnAdmins = document.getElementById('btn-admin-subtab-administradores'); const btnFacturacion = document.getElementById('btn-admin-subtab-facturacion'); if (btnRadar) btnRadar.style.display = 'inline-flex'; if (btnLiquidaciones) btnLiquidaciones.style.display = 'inline-flex'; if (isOperador) { // OPERADOR: Solo radar de despacho y liquidaciones de despacho if (btnTarifas) btnTarifas.style.display = 'none'; if (btnCupones) btnCupones.style.display = 'none'; if (btnAdmins) btnAdmins.style.display = 'none'; if (btnFacturacion) btnFacturacion.style.display = 'none'; // Si estaba en un subtab no autorizado, cambiar a radar if (['tarifas', 'cupones', 'administradores', 'facturacion'].includes(window.activeAdminSubTab)) { window.activeAdminSubTab = 'radar'; } } else { // ADMIN o SUPERADMIN: Acceso completo a todas las 6 opciones if (btnTarifas) btnTarifas.style.display = 'inline-flex'; if (btnCupones) btnCupones.style.display = 'inline-flex'; if (btnAdmins) btnAdmins.style.display = 'inline-flex'; if (btnFacturacion) btnFacturacion.style.display = 'inline-flex'; } }; window.setAdminSubTab = function (targetViewId) { window.activeAdminSubTab = targetViewId; const allViews = [ { id: 'radar', el: document.getElementById('admin-view-radar') }, { id: 'liquidaciones', el: document.getElementById('admin-view-liquidaciones') }, { id: 'tarifas', el: document.getElementById('admin-view-tarifas') }, { id: 'cupones', el: document.getElementById('admin-view-cupones') }, { id: 'administradores', el: document.getElementById('admin-view-administradores') }, { id: 'facturacion', el: document.getElementById('admin-view-facturacion') } ]; allViews.forEach(v => { if (v.el) { if (v.id === targetViewId) { v.style.display = 'block'; v.classList.add('admin-panel-section'); } else { v.style.display = 'none'; v.classList.remove('admin-panel-section'); } } }); // Actualizar indicador y estado activo en la barra de menú administrativa document.querySelectorAll('#admin-role-nav-menubar .menu-item-btn').forEach(btn => { const subtab = btn.getAttribute('data-subtab'); if (subtab === targetViewId) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); if (targetViewId === 'radar' && leafletMap) { setTimeout(() => leafletMap.invalidateSize(), 200); } if (targetViewId === 'liquidaciones') { if (window.fetchSettlementsOverview) window.fetchSettlementsOverview(); if (window.fetchAdminPayoutRequests) window.fetchAdminPayoutRequests(); } if (targetViewId === 'tarifas' && typeof fetchTariffs === 'function') { fetchTariffs(); } if (targetViewId === 'cupones' && window.fetchAdminCoupons) { window.fetchAdminCoupons(); } if (targetViewId === 'administradores' && window.fetchAdministrators) { window.fetchAdministrators(); } if (targetViewId === 'facturacion' && window.fetchMonthlyBillingReport) { window.fetchMonthlyBillingReport(); } }; // Asignar listeners a todos los botones del menú de administración document.querySelectorAll('#admin-role-nav-menubar .menu-item-btn').forEach(btn => { btn.addEventListener('click', () => { const subtab = btn.getAttribute('data-subtab'); if (subtab && typeof window.setAdminSubTab === 'function') { window.setAdminSubTab(subtab); } }); }); // ========================================================================== // BARRA DE MENÚ OFICIAL DEL CONDUCTOR // ========================================================================== window.setDriverSubTab = function (targetViewId) { window.activeDriverSubTab = targetViewId; const viewDriverWallet = document.getElementById('driver-view-billetera'); const viewDriverRegistro = document.getElementById('driver-view-registro'); if (viewDriverWallet) { viewDriverWallet.style.display = (targetViewId === 'billetera') ? 'block' : 'none'; if (targetViewId === 'billetera') viewDriverWallet.classList.add('admin-panel-section'); } if (viewDriverRegistro) { viewDriverRegistro.style.display = (targetViewId === 'registro') ? 'block' : 'none'; if (targetViewId === 'registro') viewDriverRegistro.classList.add('admin-panel-section'); } document.querySelectorAll('#driver-role-nav-menubar .menu-item-btn').forEach(btn => { const subtab = btn.getAttribute('data-subtab'); if (subtab === targetViewId) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); if (targetViewId === 'billetera') { if (window.fetchDriverWallet) window.fetchDriverWallet(); if (window.fetchDriverPayoutRequests) window.fetchDriverPayoutRequests(); } }; document.querySelectorAll('#driver-role-nav-menubar .menu-item-btn').forEach(btn => { btn.addEventListener('click', () => { const subtab = btn.getAttribute('data-subtab'); if (subtab && typeof window.setDriverSubTab === 'function') { window.setDriverSubTab(subtab); } }); }); // Enrutamiento por Hash en la URL (#admin, #conductor, etc.) function handleUrlHash() { const hash = (window.location.hash || '').toLowerCase(); if (hash === '#admin') { window.showView('admin'); } else if (hash === '#conductor' || hash === '#driver') { window.showView('driver'); } else { window.showView('public'); if (hash === '#fletes') { window.activatePublicService('tab-fletes'); } } } window.addEventListener('hashchange', handleUrlHash); handleUrlHash(); } /* ========================================================================== 3. AUTHENTICATION (PACIFIC TRANS GO CON OTP WHATSAPP + EMAIL) ========================================================================== */ function initAuthSystem() { const modal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone'); const btnOpen = document.getElementById('btn-open-auth-modal'); const btnClose = document.getElementById('btn-close-auth-modal'); const btnLogout = document.getElementById('btn-logout'); const modeLoginBtn = document.getElementById('btn-auth-mode-login'); const modeRegisterBtn = document.getElementById('btn-auth-mode-register'); const groupName = document.getElementById('group-auth-name'); const authModalTitle = document.getElementById('auth-modal-title'); const step1 = document.getElementById('auth-step-1'); const step2 = document.getElementById('auth-step-2'); const formStep1 = document.getElementById('form-auth-request-otp'); const btnVerifyOtp = document.getElementById('btn-submit-verify-otp'); const btnResend = document.getElementById('btn-auth-resend-otp'); const reqError = document.getElementById('auth-request-error'); const verError = document.getElementById('auth-verify-error'); const targetLabel = document.getElementById('auth-sent-target'); let authMode = 'login'; // 'login' | 'register' let currentIdentifier = ''; let currentEmail = ''; // Abrir y cerrar modal if (btnOpen && modal) { btnOpen.addEventListener('click', () => { reqError.style.display = 'none'; verError.style.display = 'none'; step1.style.display = 'block'; step2.style.display = 'none'; modal.classList.add('active'); }); } if (btnClose && modal) { btnClose.addEventListener('click', () => modal.classList.remove('active')); modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('active'); }); } // Alternar entre Login y Registro if (modeLoginBtn && modeRegisterBtn) { modeLoginBtn.addEventListener('click', () => { authMode = 'login'; modeLoginBtn.classList.add('active'); modeRegisterBtn.classList.remove('active'); if (groupName) groupName.style.display = 'none'; if (authModalTitle) authModalTitle.textContent = 'Acceso Seguro con Verificación OTP'; }); modeRegisterBtn.addEventListener('click', () => { authMode = 'register'; modeRegisterBtn.classList.add('active'); modeLoginBtn.classList.remove('active'); if (groupName) groupName.style.display = 'block'; if (authModalTitle) authModalTitle.textContent = 'Registro de Usuario & Validación 2FA'; }); } // Paso 1: Enviar OTP por WhatsApp if (formStep1) { formStep1.addEventListener('submit', async (e) => { e.preventDefault(); const phoneInput = document.getElementById('auth-input-phone'); const emailInput = document.getElementById('auth-input-email'); const nameInput = document.getElementById('auth-input-name'); const btnSubmit = document.getElementById('btn-submit-send-otp'); const rawPhone = phoneInput?.value.trim() || ''; const email = emailInput?.value.trim() || ''; const name = nameInput?.value.trim() || ''; let cleanPhone = rawPhone.replace(/\D/g, ''); if (cleanPhone.startsWith('57') && cleanPhone.length === 12) { // Prefijo 57 incluido } else if (cleanPhone.length === 10) { cleanPhone = '57' + cleanPhone; } if (!cleanPhone || cleanPhone.length < 10) { showError(reqError, 'Ingresa un número de celular válido (Ej: 300 123 4567).'); return; } currentIdentifier = cleanPhone; currentEmail = email; btnSubmit.disabled = true; btnSubmit.innerHTML = '⏳ Enviando código de seguridad...'; reqError.style.display = 'none'; try { const res = await fetch('/api/auth/send-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: cleanPhone, email }) }); const data = await res.json(); if (res.ok && data.success) { step1.style.display = 'none'; step2.style.display = 'block'; if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${cleanPhone}`; verError.style.display = 'none'; verError.innerHTML = ''; focusFirstOtp(); } else { showError(reqError, data.error || 'No se pudo enviar el código OTP.'); } } catch (err) { showError(reqError, 'Error de conexión. Inténtalo de nuevo.'); } finally { btnSubmit.disabled = false; btnSubmit.innerHTML = '📲 Enviar Código de Verificación OTP'; } }); } // Cajas OTP con avance automático y soporte de pegado const otpInputs = [ document.getElementById('otp-1'), document.getElementById('otp-2'), document.getElementById('otp-3'), document.getElementById('otp-4'), document.getElementById('otp-5'), document.getElementById('otp-6') ]; otpInputs.forEach((input, index) => { if (!input) return; input.addEventListener('input', (e) => { const val = e.target.value; if (val.length === 1 && index < 5) { otpInputs[index + 1].focus(); } checkAutoSubmitOtp(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !input.value && index > 0) { otpInputs[index - 1].focus(); } }); input.addEventListener('paste', (e) => { e.preventDefault(); const pasteData = e.clipboardData.getData('text').trim(); if (/^\d{6}$/.test(pasteData)) { pasteData.split('').forEach((digit, i) => { if (otpInputs[i]) otpInputs[i].value = digit; }); checkAutoSubmitOtp(); } }); }); function focusFirstOtp() { otpInputs.forEach(i => { if (i) i.value = ''; }); if (otpInputs[0]) otpInputs[0].focus(); } function getEnteredOtp() { return otpInputs.map(i => i?.value || '').join(''); } function checkAutoSubmitOtp() { const code = getEnteredOtp(); if (code.length === 6) { submitVerifyOtp(code); } } // Paso 2: Verificar OTP if (btnVerifyOtp) { btnVerifyOtp.addEventListener('click', () => { const code = getEnteredOtp(); if (code.length < 6) { showError(verError, 'Ingresa los 6 dígitos del código.'); return; } submitVerifyOtp(code); }); } async function submitVerifyOtp(code) { if (!btnVerifyOtp) return; btnVerifyOtp.disabled = true; btnVerifyOtp.innerHTML = '⏳ Verificando código...'; verError.style.display = 'none'; const nameInput = document.getElementById('auth-input-name'); const name = nameInput?.value.trim() || ''; try { const res = await fetch('/api/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: currentIdentifier, code, nombre: name, email: currentEmail }) }); const data = await res.json(); if (res.ok && data.success) { const expiresAt = data.expiresAt || (Date.now() + 24 * 60 * 60 * 1000); localStorage.setItem('pt_token', data.token); localStorage.setItem('pt_token_expires_at', String(expiresAt)); currentUser = data.user; updateUserUI(currentUser); modal.classList.remove('active'); alert(`¡Bienvenido a Pacific Trans GO, ${currentUser.nombre}! Tu identidad ha sido verificada con éxito.`); } else { showError(verError, data.error || 'Código incorrecto. Verifica los 6 dígitos.'); } } catch (err) { showError(verError, 'Error al validar el código OTP.'); } finally { btnVerifyOtp.disabled = false; btnVerifyOtp.innerHTML = '✅ Validar y Acceder'; } } // Reenviar OTP if (btnResend) { btnResend.addEventListener('click', () => { formStep1.dispatchEvent(new Event('submit')); }); } // Cerrar sesión if (btnLogout) { btnLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); localStorage.removeItem('pt_token_expires_at'); currentUser = null; updateUserUI(null); window.location.hash = '#reservas'; window.showView('public'); }); } // Validar sesión persistida (vigencia de 24 horas) checkPersistedSession(); } async function checkPersistedSession() { const token = localStorage.getItem('pt_token'); const expiresAt = localStorage.getItem('pt_token_expires_at'); if (!token) return; // Comprobar lapso de 24 horas en el navegador if (expiresAt && Date.now() > Number(expiresAt)) { console.log('⏰ [Sesión Pacific Trans GO]: El token de 24 horas ha expirado. Requiere autenticación OTP.'); localStorage.removeItem('pt_token'); localStorage.removeItem('pt_token_expires_at'); currentUser = null; updateUserUI(null); return; } try { const res = await fetch(`/api/auth/me?token=${encodeURIComponent(token)}`); if (res.ok) { const data = await res.json(); if (data.user) { currentUser = data.user; if (data.user.tokenExpiresAt) { localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt)); } updateUserUI(currentUser); console.log(`✅ [Sesión Autenticada]: ${currentUser.nombre} (+${currentUser.telefono}) activo por 24 horas.`); } else { localStorage.removeItem('pt_token'); localStorage.removeItem('pt_token_expires_at'); } } else { localStorage.removeItem('pt_token'); localStorage.removeItem('pt_token_expires_at'); } } catch (err) { console.warn('⚠️ [Session Verify Error]:', err); } } // Helper centralizado para rellenar por defecto los formularios con la información del usuario autenticado function autofillFormsWithUserData(user) { if (!user) return; // 1. VIAJES DE PASAJEROS: Dirección principal y datos de Pasajero 1 (Titular) const caliAddrInput = document.getElementById('cali-address'); if (caliAddrInput && (!caliAddrInput.value || caliAddrInput.value.trim() === '') && user.direccionTexto) { caliAddrInput.value = user.direccionTexto; } if (typeof window.setPassenger1Data === 'function') { window.setPassenger1Data(user); } else { const p1Name = document.getElementById('passenger-1-name'); const p1Doc = document.getElementById('passenger-1-doc'); const p1Phone = document.getElementById('passenger-1-phone'); const p1Addr = document.getElementById('passenger-1-addr'); if (p1Name && (!p1Name.value || p1Name.value.trim() === '' || p1Name.value.startsWith('Usuario '))) { p1Name.value = user.nombre || ''; } if (p1Doc && (!p1Doc.value || p1Doc.value.trim() === '') && user.documentoIdentidad) { p1Doc.value = user.documentoIdentidad; } if (p1Phone && (!p1Phone.value || p1Phone.value.trim() === '')) { p1Phone.value = user.telefono || ''; } if (p1Addr && (!p1Addr.value || p1Addr.value.trim() === '') && user.direccionTexto) { p1Addr.value = user.direccionTexto; } } // 2. MENSAJERÍA & ENCOMIENDAS (FLETES): Datos del Remitente const senderNameInput = document.getElementById('sender-name'); const senderPhoneInput = document.getElementById('sender-phone'); if (senderNameInput && (!senderNameInput.value || senderNameInput.value.trim() === '') && user.nombre) { senderNameInput.value = user.nombre; } if (senderPhoneInput && (!senderPhoneInput.value || senderPhoneInput.value.trim() === '') && user.telefono) { senderPhoneInput.value = user.telefono; } } window.autofillFormsWithUserData = autofillFormsWithUserData; 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'); if (user) { if (btnAuthOpen) btnAuthOpen.style.display = 'none'; if (userBadge) userBadge.style.display = 'inline-flex'; if (userNameEl) userNameEl.textContent = (user.nombre || 'Usuario').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'; } // Rellenar por defecto los formularios de viajes y encomiendas con los datos del usuario autenticado autofillFormsWithUserData(user); // Actualizar vista actual si está en zona protegida const hash = (window.location.hash || '').toLowerCase(); if (hash === '#admin') window.showView('admin'); if (hash === '#conductor' || hash === '#driver') window.showView('driver'); } else { if (btnAuthOpen) btnAuthOpen.style.display = 'inline-flex'; if (userBadge) userBadge.style.display = 'none'; if (navBtnAdmin) navBtnAdmin.style.display = 'none'; if (navBtnDriver) navBtnDriver.style.display = 'none'; const hash = (window.location.hash || '').toLowerCase(); if (hash === '#admin' || hash === '#conductor' || hash === '#driver') { window.showView(hash === '#admin' ? 'admin' : 'driver'); } } // Actualizar notificaciones para el usuario actual (o limpiar si cerró sesión) if (typeof window.renderUserNotifications === 'function') { window.renderUserNotifications(); } } function showError(el, msg) { if (!el) return; el.textContent = msg; el.style.display = 'block'; } /* ========================================================================== 4. LEAFLET RADAR FLEET MAP (MAPA ESTÁNDAR CON TOOLTIPS) ========================================================================== */ function initLeafletRadarMap() { const mapContainer = document.getElementById('fleet-live-map'); if (!mapContainer || typeof L === 'undefined') return; // Centro entre Cali (3.45) y Buenaventura (3.88) const corridorCenter = [3.65, -76.78]; leafletMap = L.map('fleet-live-map', { center: corridorCenter, zoom: 10, zoomControl: true, scrollWheelZoom: false }); // Capa de Mapa Estándar OpenStreetMap L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '© OpenStreetMap contributors • Pacific Trans GO GPS' }).addTo(leafletMap); // Línea poligonal oficial de la Carretera Vía al Mar (Ruta 40) const corridorWaypoints = [ [3.4516, -76.5320], // Cali [3.4750, -76.5650], // Portada al Mar [3.5800, -76.6300], // Km 18 / Saladito [3.6580, -76.6900], // Dagua [3.7650, -76.7350], // Loboguerrero [3.7920, -76.8150], // Cisneros [3.8450, -76.9200], // Córdoba [3.8814, -77.0315] // Buenaventura ]; L.polyline(corridorWaypoints, { color: '#06b6d4', weight: 5, opacity: 0.85, dashArray: '8, 6' }).addTo(leafletMap); // Marcadores de Hitos Urbanos addCityMarker(leafletMap, [3.4516, -76.5320], '🟢 Cali (Km 0)'); addCityMarker(leafletMap, [3.6580, -76.6900], '🔵 Dagua (Km 45)'); addCityMarker(leafletMap, [3.7650, -76.7350], '🔵 Loboguerrero (Km 60)'); addCityMarker(leafletMap, [3.8814, -77.0315], '🟡 Buenaventura (Km 115)'); // Vehículos de la Flota en Tiempo Real activeDrivers.forEach((driver, index) => { const customIcon = L.divIcon({ className: 'vehicle-marker-icon', html: `
🚗
`, iconSize: [44, 44], iconAnchor: [22, 22] }); const marker = L.marker([driver.lat, driver.lng], { icon: customIcon }).addTo(leafletMap); // Tooltip enriquecido al pasar el cursor o pulsar const tooltipContent = `
#${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 oficiales const elGroupPacific = document.getElementById('cfg-group-pacifictrans'); const adminWaGroupBadge = document.getElementById('admin-wa-group-badge'); if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija; if (elBasePaquete) elBasePaquete.value = currentTariffs.precioBasePaquete; if (elKmPaquete) elKmPaquete.value = currentTariffs.precioKmPaquete; if (elSobre && currentTariffs.factorPaquetes) elSobre.value = currentTariffs.factorPaquetes.sobre || 1.0; if (elMediano && currentTariffs.factorPaquetes) elMediano.value = currentTariffs.factorPaquetes.mediano || 1.6; if (elGrande && currentTariffs.factorPaquetes) elGrande.value = currentTariffs.factorPaquetes.grande || 2.2; if (elSeguro) elSeguro.value = currentTariffs.seguroBasePaquete; const elRecargoUrgente = document.getElementById('cfg-recargo-urgente'); if (elRecargoUrgente) elRecargoUrgente.value = currentTariffs.recargoEnvioUrgente ?? 15000; const urgentLabel = document.getElementById('freight-urgent-amount-label'); if (urgentLabel) urgentLabel.textContent = '$' + (currentTariffs.recargoEnvioUrgente ?? 15000).toLocaleString('es-CO'); const urgentCost = document.getElementById('freight-urgent-cost'); if (urgentCost) urgentCost.textContent = '+$' + (currentTariffs.recargoEnvioUrgente ?? 15000).toLocaleString('es-CO') + ' COP'; if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin; if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion; if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros; if (elPhone && currentTariffs.numeroAtencionWhatsApp) elPhone.value = currentTariffs.numeroAtencionWhatsApp; if (elName && currentTariffs.nombreSoporte) elName.value = currentTariffs.nombreSoporte; const pacificGroup = currentTariffs.grupoWhatsAppPacificTrans || 'Pacific Trans Notificaciones'; if (elGroupPacific) elGroupPacific.value = pacificGroup; if (adminWaGroupBadge) adminWaGroupBadge.textContent = `👥 Grupo: ${pacificGroup}`; // 2. Sincronización del enlace dinámico de atención WhatsApp en el footer y navbar const phoneFormatted = formatPhoneNumber(currentTariffs.numeroAtencionWhatsApp || '573167455877'); const cleanPhone = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, ''); const footerPhoneEl = document.getElementById('footer-support-phone'); const footerLinkEl = document.getElementById('footer-support-link'); if (footerPhoneEl) footerPhoneEl.textContent = phoneFormatted; if (footerLinkEl) footerLinkEl.href = `https://wa.me/${cleanPhone}?text=Hola%20Pacific%20Trans%20GO,%20deseo%20informaci%C3%B3n%20sobre%20un%20servicio`; // 3. Precios en módulo de reservas de pasajeros if (window.updatePassengerBookingSummary) { window.updatePassengerBookingSummary(); } else { const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`; const totalPriceEl = document.querySelector('.total-price'); if (totalPriceEl) totalPriceEl.textContent = formattedPasajero; } // 4. Recalcular módulo de mensajería y fletes if (window.recalculateFreight) { window.recalculateFreight(); } } function formatPhoneNumber(num) { const clean = num.replace(/\D/g, ''); if (clean.startsWith('57') && clean.length === 12) { return `+57 ${clean.slice(2, 5)} ${clean.slice(5, 8)} ${clean.slice(8)}`; } return `+${clean}`; } function initTariffsManager() { const form = document.getElementById('form-tariffs-config'); const statusEl = document.getElementById('tariffs-save-status'); const btnDetect = document.getElementById('btn-detect-wa-groups'); // Auto-Detección de Grupos WhatsApp mediante Evolution API if (btnDetect) { btnDetect.addEventListener('click', async () => { btnDetect.disabled = true; const originalText = btnDetect.innerHTML; btnDetect.innerHTML = '⏳ Detectando grupos...'; try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/admin/whatsapp/groups', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (res.ok && data.success) { const inputPacific = document.getElementById('cfg-group-pacifictrans'); const badgePacific = document.getElementById('badge-pacific-jid'); if (inputPacific && data.pacificTrans?.groupName) { inputPacific.value = data.pacificTrans.groupName; } if (badgePacific && data.pacificTrans?.resolvedJid) { badgePacific.innerHTML = `✓ Destino: ${data.pacificTrans.resolvedJid} (${data.pacificTrans.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`; badgePacific.style.color = '#34d399'; } alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacific Trans GO ➔ ${data.pacificTrans?.groupName || 'Configurado'} (${data.pacificTrans?.resolvedJid || 'JID Asignado'})`); } else { alert('No se pudieron consultar los grupos: ' + (data.error || 'Verifica la sesión')); } } catch (err) { console.error('Error detect groups:', err); alert('Error al consultar Evolution API'); } finally { btnDetect.disabled = false; btnDetect.innerHTML = originalText; } }); } if (!form) return; form.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('btn-save-tariffs'); if (btn) { btn.disabled = true; btn.innerHTML = '⏳ Guardando en MongoDB...'; } const payload = { tarifaPasajeroFija: parseFloat(document.getElementById('cfg-tarifa-pasajero')?.value || '35000'), precioBasePaquete: parseFloat(document.getElementById('cfg-base-paquete')?.value || '25000'), precioKmPaquete: parseFloat(document.getElementById('cfg-km-paquete')?.value || '1200'), factorPaquetes: { sobre: parseFloat(document.getElementById('cfg-factor-sobre')?.value || '1.0'), mediano: parseFloat(document.getElementById('cfg-factor-mediano')?.value || '1.6'), grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2') }, seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'), recargoEnvioUrgente: parseFloat(document.getElementById('cfg-recargo-urgente')?.value || '15000'), porcentajeComisionAdmin: parseFloat(document.getElementById('cfg-comision')?.value || '15'), costoPenalidadCancelacion: parseFloat(document.getElementById('cfg-penalidad')?.value || '15000'), radioMaxPoolingMetros: parseFloat(document.getElementById('cfg-radio-pooling')?.value || '4000'), numeroAtencionWhatsApp: document.getElementById('cfg-admin-phone')?.value.trim() || '573167455877', nombreSoporte: document.getElementById('cfg-admin-name')?.value.trim() || 'Atención Pacific Trans GO', grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans Notificaciones' }; try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/tariffs', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); if (res.ok) { const data = await res.json(); currentTariffs = { ...currentTariffs, ...data.config }; syncTariffsToUI(); if (statusEl) { statusEl.innerHTML = `✅ ¡Configuración guardada y sincronizada! Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Grupo Oficial: "${payload.grupoWhatsAppPacificTrans}"`; statusEl.style.color = '#10b981'; } } else { throw new Error('Error al guardar en el servidor'); } } catch (err) { currentTariffs = { ...currentTariffs, ...payload }; syncTariffsToUI(); if (statusEl) { statusEl.innerHTML = `✅ Configuración actualizada localmente.`; statusEl.style.color = '#10b981'; } } finally { if (btn) { btn.disabled = false; btn.innerHTML = '💾 Guardar y Aplicar Configuración'; } } }); } /* ========================================================================== 7. RESERVA DE PASAJEROS & POOLING ========================================================================== */ function calculateSuggestedTime(timeStr, hoursDiff) { if (!timeStr) return null; const parts = timeStr.split(':'); if (parts.length < 2) return null; const h = parseInt(parts[0], 10); const m = parseInt(parts[1], 10); if (isNaN(h) || isNaN(m)) return null; let totalMins = h * 60 + m + Math.round(hoursDiff * 60); while (totalMins < 0) totalMins += 24 * 60; totalMins = totalMins % (24 * 60); const newH = Math.floor(totalMins / 60); const newM = totalMins % 60; const period = newH >= 12 ? 'PM' : 'AM'; const displayH = newH % 12 === 0 ? 12 : newH % 12; const displayM = String(newM).padStart(2, '0'); return { time24: `${String(newH).padStart(2, '0')}:${String(newM).padStart(2, '0')}`, display: `${displayH}:${displayM} ${period}` }; } 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'); // Modalidad de Tiempo: Inmediato vs Programado let poolTimeMode = 'INMEDIATO'; // 'INMEDIATO' | 'PROGRAMADO' let poolScheduleType = 'HORA_RECOGIDA'; // 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO' const btnPoolImmediate = document.getElementById('btn-pool-mode-immediate'); const btnPoolScheduled = document.getElementById('btn-pool-mode-scheduled'); const poolScheduledOptions = document.getElementById('pool-scheduled-options'); const poolTimeBadge = document.getElementById('pool-time-badge'); const poolRadioRecogida = document.getElementById('pool-type-recogida'); const poolRadioDestino = document.getElementById('pool-type-destino'); const poolDateInput = document.getElementById('pool-schedule-date'); const poolTimeInput = document.getElementById('pool-schedule-time'); const poolTimeLabel = document.getElementById('pool-label-time'); const poolSmartEta = document.getElementById('pool-smart-eta'); const poolSummaryScheduleText = document.getElementById('pool-summary-schedule-text'); let selectedSeats = 1; let selectedPaymentMethod = 'BRE_B'; let currentSeatsFilled = 3; // Cache para persistir los datos ingresados al alternar entre 1..4 cupos const passengersData = [ { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }, { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }, { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }, { nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' } ]; function saveCurrentFormData() { for (let i = 1; i <= selectedSeats; i++) { const nameInput = document.getElementById(`passenger-${i}-name`); const docInput = document.getElementById(`passenger-${i}-doc`); const phoneInput = document.getElementById(`passenger-${i}-phone`); const notesInput = document.getElementById(`passenger-${i}-notes`); const sameAddrCheck = document.getElementById(`passenger-${i}-same-addr`); const addrInput = document.getElementById(`passenger-${i}-addr`); if (nameInput) passengersData[i - 1].nombre = nameInput.value; if (docInput) passengersData[i - 1].doc = docInput.value; if (phoneInput) passengersData[i - 1].phone = phoneInput.value; if (notesInput) passengersData[i - 1].notes = notesInput.value; if (sameAddrCheck) passengersData[i - 1].sameAddress = sameAddrCheck.checked; if (addrInput) passengersData[i - 1].address = addrInput.value; } } // Helper para asignar los datos del Pasajero 1 (Titular) desde el usuario autenticado window.setPassenger1Data = function (user) { if (!user) return; if (passengersData && passengersData[0]) { if (!passengersData[0].nombre || passengersData[0].nombre.startsWith('Usuario ')) { passengersData[0].nombre = user.nombre || ''; } if (!passengersData[0].phone) passengersData[0].phone = user.telefono || ''; if (!passengersData[0].doc && user.documentoIdentidad) passengersData[0].doc = user.documentoIdentidad; if (!passengersData[0].address && user.direccionTexto) passengersData[0].address = user.direccionTexto; } const p1Name = document.getElementById('passenger-1-name'); const p1Doc = document.getElementById('passenger-1-doc'); const p1Phone = document.getElementById('passenger-1-phone'); const p1Addr = document.getElementById('passenger-1-addr'); if (p1Name && (!p1Name.value || p1Name.value.trim() === '' || p1Name.value.startsWith('Usuario '))) { p1Name.value = user.nombre || ''; } if (p1Doc && (!p1Doc.value || p1Doc.value.trim() === '') && user.documentoIdentidad) { p1Doc.value = user.documentoIdentidad; } if (p1Phone && (!p1Phone.value || p1Phone.value.trim() === '')) { p1Phone.value = user.telefono || ''; } if (p1Addr && (!p1Addr.value || p1Addr.value.trim() === '') && user.direccionTexto) { p1Addr.value = user.direccionTexto; } const caliAddr = document.getElementById('cali-address'); if (caliAddr && (!caliAddr.value || caliAddr.value.trim() === '') && user.direccionTexto) { caliAddr.value = user.direccionTexto; } }; function renderPassengerCards(count) { if (!passengersContainer) return; // Si hay usuario logueado, prellenar por defecto los datos del Pasajero 1 (Titular) if (currentUser) { if (!passengersData[0].nombre || passengersData[0].nombre.startsWith('Usuario ')) { passengersData[0].nombre = currentUser.nombre || ''; } if (!passengersData[0].phone) passengersData[0].phone = currentUser.telefono || ''; if (!passengersData[0].doc && currentUser.documentoIdentidad) passengersData[0].doc = currentUser.documentoIdentidad; if (!passengersData[0].address && currentUser.direccionTexto) passengersData[0].address = currentUser.direccionTexto; } 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; } // Actualizar texto del badge informativo según el método de pago const gatewayBadgeText = document.getElementById('summary-gateway-badge-text'); if (gatewayBadgeText) { if (selectedPaymentMethod === 'BRE_B') { gatewayBadgeText.innerHTML = '⚡ Transferencia directa por QR Bre-B (Inmediato 24/7 sin comisión)'; } else if (selectedPaymentMethod === 'BOLD') { gatewayBadgeText.innerHTML = '💳 Pago seguro en línea (Tarjetas Débito / Crédito, PSE y Nequi)'; } else if (selectedPaymentMethod === 'NEQUI') { gatewayBadgeText.innerHTML = '📱 Transferencia directa a Nequi'; } else { gatewayBadgeText.innerHTML = '💵 Pago en efectivo directamente al conductor al abordar'; } } if (btnBookText) { const methodText = selectedPaymentMethod === 'BRE_B' ? 'Pagar con QR Bre-B' : (selectedPaymentMethod === 'BOLD' ? 'Pagar en Línea' : (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar (Efectivo)')); btnBookText.textContent = `${methodText} ${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'} (${formattedTotal})`; } // Actualizar previsualización en dots del vehículo updateSeatVisuals(selectedSeats); updatePoolScheduleCalculation(); } function updatePoolScheduleCalculation() { if (poolTimeMode === 'INMEDIATO') { if (poolTimeBadge) { poolTimeBadge.textContent = '⚡ Salida Inmediata'; poolTimeBadge.className = 'badge-time-selected'; } if (poolScheduledOptions) poolScheduledOptions.style.display = 'none'; if (poolSummaryScheduleText) poolSummaryScheduleText.textContent = '⚡ Inmediato (Próxima Salida)'; return; } if (poolTimeBadge) { poolTimeBadge.textContent = '📅 Viaje Programado'; poolTimeBadge.className = 'badge-time-selected'; } if (poolScheduledOptions) poolScheduledOptions.style.display = 'block'; const chosenDate = poolDateInput ? poolDateInput.value : ''; const chosenTime = poolTimeInput ? poolTimeInput.value : ''; if (!chosenTime) { if (poolSmartEta) poolSmartEta.style.display = 'none'; if (poolSummaryScheduleText) poolSummaryScheduleText.textContent = chosenDate ? `📅 Programado para el ${chosenDate}` : '📅 Programado'; return; } if (poolScheduleType === 'HORA_LLEGADA_DESTINO') { if (poolTimeLabel) poolTimeLabel.textContent = 'Hora que requieres estar en destino'; const suggested = calculateSuggestedTime(chosenTime, -3); // Cali - Buenaventura ~3 horas if (poolSmartEta && suggested) { poolSmartEta.style.display = 'block'; poolSmartEta.innerHTML = `🚗💡 Planificación Inteligente: Para llegar a tu destino a las ${chosenTime}, tu vehículo te recogerá aproximadamente a las ${suggested.display} (trayecto ~3 hrs en el corredor).`; } if (poolSummaryScheduleText) { poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}
🏁 Destino: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}`; } } else { if (poolTimeLabel) poolTimeLabel.textContent = 'Hora deseada de recogida'; const estimated = calculateSuggestedTime(chosenTime, +3); if (poolSmartEta && estimated) { poolSmartEta.style.display = 'block'; poolSmartEta.innerHTML = `🏁💡 Estimación de Ruta: Si te recogemos a las ${chosenTime}, tu llegada estimada a destino será alrededor de las ${estimated.display} (~3 hrs en carretera).`; } if (poolSummaryScheduleText) { poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}
🚪 Recogida: ${chosenTime} | 🏁 Llegada aprox: ~${estimated ? estimated.display : ''}`; } } } // Inicializar fecha mínima en hoy y hora por defecto if (poolDateInput) { const today = new Date().toISOString().split('T')[0]; poolDateInput.min = today; if (!poolDateInput.value) poolDateInput.value = today; } if (poolTimeInput && !poolTimeInput.value) { const now = new Date(Date.now() + 60 * 60 * 1000); const hours = String(now.getHours()).padStart(2, '0'); const mins = String(now.getMinutes()).padStart(2, '0'); poolTimeInput.value = `${hours}:${mins}`; } if (btnPoolImmediate && btnPoolScheduled) { btnPoolImmediate.addEventListener('click', () => { poolTimeMode = 'INMEDIATO'; btnPoolImmediate.classList.add('active'); btnPoolScheduled.classList.remove('active'); updatePoolScheduleCalculation(); }); btnPoolScheduled.addEventListener('click', () => { poolTimeMode = 'PROGRAMADO'; btnPoolScheduled.classList.add('active'); btnPoolImmediate.classList.remove('active'); updatePoolScheduleCalculation(); }); } if (poolRadioRecogida && poolRadioDestino) { poolRadioRecogida.addEventListener('change', () => { if (poolRadioRecogida.checked) poolScheduleType = 'HORA_RECOGIDA'; updatePoolScheduleCalculation(); }); poolRadioDestino.addEventListener('change', () => { if (poolRadioDestino.checked) poolScheduleType = 'HORA_LLEGADA_DESTINO'; updatePoolScheduleCalculation(); }); } if (poolDateInput) poolDateInput.addEventListener('change', updatePoolScheduleCalculation); if (poolTimeInput) poolTimeInput.addEventListener('input', updatePoolScheduleCalculation); 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'; const caliAddressLabel = document.querySelector('label[for="cali-address"]'); const buenaStopLabel = document.querySelector('label[for="buena-stop"]'); if (addressGroup) addressGroup.style.display = 'block'; if (buenaStopGroup) buenaStopGroup.style.display = 'block'; if (caliAddressLabel) { caliAddressLabel.textContent = isCali ? '📍 Dirección Principal de Recogida en Cali (Domicilio Puerta a Puerta)' : '🏁 Dirección Principal de Llegada en Cali (Entrega en Domicilio)'; } if (buenaStopLabel) { buenaStopLabel.textContent = isCali ? '🏁 Parada de Destino en Buenaventura' : '📍 Punto Oficial de Salida en Buenaventura'; } } if (originSelect) { originSelect.addEventListener('change', updateOriginDestination); updateOriginDestination(); } // Render inicial renderPassengerCards(selectedSeats); updateSummary(); // Si el usuario ya está autenticado al cargar el módulo, prellenar sus datos en Pasajero 1 y dirección if (currentUser) { window.setPassenger1Data(currentUser); } // Envío del Formulario y Reserva en MongoDB con Pooling if (bookBtn) { bookBtn.addEventListener('click', async (e) => { e.preventDefault(); saveCurrentFormData(); // Validar programación si aplica if (poolTimeMode === 'PROGRAMADO') { const pDate = poolDateInput ? poolDateInput.value : ''; const pTime = poolTimeInput ? poolTimeInput.value : ''; if (!pDate) { alert('📅 Por favor selecciona la fecha programada para tu viaje.'); poolDateInput?.focus(); return; } if (!pTime) { alert('🕒 Por favor indica la hora para la programación de tu viaje.'); poolTimeInput?.focus(); return; } } // 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 || (isCaliOrigin ? 'Cali, Valle del Cauca' : 'Buenaventura, Valle del Cauca')); const pCoords = (i === 1 && window.passengerGpsCoords) ? window.passengerGpsCoords : (isCaliOrigin ? [-76.5320, 3.4215] : [-77.0250, 3.8820]); validatedPassengers.push({ nombre: d.nombre.trim(), documentoIdentidad: d.doc.trim(), telefono: d.phone.trim(), direccionRecogida: pAddress, coordenadas: pCoords, notas: d.notes ? d.notes.trim() : '', esTitular: (i === 1) }); } // 3. Verificación de seguridad OTP if (!currentUser) { const p1Phone = validatedPassengers[0].telefono.replace(/\D/g, ''); const authInput = document.getElementById('auth-identifier'); if (authInput && p1Phone) { authInput.value = p1Phone; } alert('🛡️ Por seguridad de los conductores y de los demás pasajeros, debes verificar tu identidad con OTP antes de confirmar tu reserva.'); (document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone'))?.classList.add('active'); return; } // 4. Procesar reserva en el Backend bookBtn.disabled = true; const originalBtnHtml = bookBtn.innerHTML; bookBtn.innerHTML = ` Agrupando ${selectedSeats} Cupo(s)... `; try { const userTripCoords = (window.passengerGpsCoords && window.passengerGpsCoords.length === 2) ? window.passengerGpsCoords : (isCaliOrigin ? [-76.5320, 3.4215] : [-77.0250, 3.8820]); const payload = { usuarioId: currentUser ? currentUser._id : undefined, sentido: isCaliOrigin ? 'CALI_A_BUENAVENTURA' : 'BUENAVENTURA_A_CALI', direccionTexto: caliAddress || (isCaliOrigin ? 'Cali, Valle del Cauca' : 'Buenaventura, Valle del Cauca'), coordenadas: userTripCoords, puntoFijoBuenaventura: { nombre: buenaStopSelect ? buenaStopSelect.options[buenaStopSelect.selectedIndex].text : 'Terminal de Transporte de Buenaventura', coordenadas: [-77.0250, 3.8820] }, metodoPago: selectedPaymentMethod, cuposSolicitados: selectedSeats, pasajeros: validatedPassengers, modalidadTiempo: poolTimeMode, tipoProgramacion: poolTimeMode === 'PROGRAMADO' ? poolScheduleType : undefined, fechaProgramada: poolTimeMode === 'PROGRAMADO' ? poolDateInput?.value : undefined, horaProgramada: poolTimeMode === 'PROGRAMADO' ? poolTimeInput?.value : undefined, horaSugeridaRecogida: (poolTimeMode === 'PROGRAMADO' && poolScheduleType === 'HORA_LLEGADA_DESTINO') ? calculateSuggestedTime(poolTimeInput?.value || '', -3)?.time24 : undefined, codigoCupon: appliedCoupon?.coupon?.codigo || undefined, email: currentUser?.email || '' }; const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/trips/pool', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok && data.success) { const totalOccupied = data.totalCuposOcupados || data.pasajerosAgrupados || 4; updateSeatVisuals(totalOccupied); // Mostrar alerta detallada const alertBox = document.getElementById('pool-dispatch-alert'); const alertTitle = document.getElementById('alert-title'); const alertMsg = document.getElementById('alert-message'); const alertBreakdown = document.getElementById('alert-passengers-breakdown'); const alertBoldArea = document.getElementById('alert-bold-payment-area'); if (alertTitle) { alertTitle.textContent = totalOccupied === 4 ? '🔥 ¡Vehículo Completo (4/4 Cupos) Listo para Despacho!' : `✅ ¡${selectedSeats} ${selectedSeats === 1 ? 'Cupo Reservado' : 'Cupos Reservados'} Exitosamente! (${totalOccupied}/4 Ocupados)`; } if (alertMsg) { alertMsg.innerHTML = ` Viaje #${data.tripId?.slice(-6) || ''} en ruta ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}.
${data.message} `; } if (alertBreakdown) { let breakdownHtml = `
👥 Pasajeros Registrados en esta Reserva:
`; validatedPassengers.forEach((p, idx) => { breakdownHtml += `
${idx + 1}. ${p.nombre} (C.C. ${p.documentoIdentidad}) • Tel: +${p.telefono} • Recogida: ${p.direccionRecogida} ${p.notas ? `• 🧳 ${p.notas}` : ''}
`; }); breakdownHtml += `
`; alertBreakdown.innerHTML = breakdownHtml; } // Renderizar información o enlace de pago según el método seleccionado if (alertBoldArea) { if (payload.metodoPago === 'BRE_B') { alertBoldArea.style.display = 'block'; const cleanWa = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, ''); alertBoldArea.innerHTML = `
⚡ Transferencia Inmediata mediante QR Bre-B

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

🏛️ Llave Bre-B Oficial (Celular): +${cleanWa}
📲 Interoperabilidad: Puedes transferir desde Bancolombia, Nequi, Daviplata, Dale o cualquier entidad financiera sin costo.
Referencia de Reserva: #${data.tripId?.slice(-6) || 'PT'}
📲 Enviar Comprobante Bre-B por WhatsApp
`; } else if (data.boldPayment && data.boldPayment.checkoutUrl) { alertBoldArea.style.display = 'block'; alertBoldArea.innerHTML = `
💳 Enlace de Pago en Línea Seguro Generado

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

👉 Abrir Pago Seguro en Línea (Tarjetas, PSE, Nequi)
`; } else { alertBoldArea.style.display = 'none'; } } if (alertBox) { alertBox.style.display = 'block'; alertBox.scrollIntoView({ behavior: 'smooth' }); } if (typeof window.addUserNotification === 'function' && currentUser) { window.addUserNotification({ id: `trip-${data.tripId || Date.now()}`, icon: '🚗', title: `¡Reserva Confirmada! (${selectedSeats} Cupo${selectedSeats > 1 ? 's' : ''})`, desc: `Viaje #${data.tripId?.slice(-6) || ''} en ruta ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}. Conductor y ruta asignados.`, time: 'Hace un momento', unread: true }, currentUser); } const formattedTotal = `$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP`; bookBtn.innerHTML = `✅ ${selectedSeats} Cupo(s) Confirmado(s) (${formattedTotal})`; } else { alert('❌ No se pudo completar la reserva: ' + (data.message || data.error || 'Intenta de nuevo')); bookBtn.innerHTML = originalBtnHtml; bookBtn.disabled = false; } } catch (err) { console.error('Error booking pool:', err); alert('⚠️ Error de conexión con el servidor de pooling. Verifica tu red.'); bookBtn.innerHTML = originalBtnHtml; bookBtn.disabled = false; } }); } function updateSeatVisuals(count) { poolDots.forEach((dot, index) => { dot.classList.remove('active', 'pending'); if (index < count) { dot.classList.add('active'); } else if (index === count) { dot.classList.add('pending'); } }); const seatCounter = document.getElementById('seat-counter-text'); if (seatCounter) seatCounter.textContent = `${count}/4 Pasajeros Confirmados`; } updateSeatVisuals(currentSeatsFilled); } /* ========================================================================== 8. CALCULADORA DE MENSAJERÍA & FLETES ========================================================================== */ function initFreightCalculator() { const packageTypeSelect = document.getElementById('package-type'); const declaredValueInput = document.getElementById('declared-value'); const totalPriceEl = document.getElementById('freight-total-price'); const baseCostEl = document.getElementById('freight-base-cost'); const distCostEl = document.getElementById('freight-dist-cost'); const insuranceCostEl = document.getElementById('freight-insurance-cost'); const freightSubtotalRow = document.getElementById('freight-subtotal-row'); const freightSubtotalPrice = document.getElementById('freight-subtotal-price'); const freightDiscountRow = document.getElementById('freight-discount-row'); const freightDiscountAmount = document.getElementById('freight-discount-amount'); const freightCouponInput = document.getElementById('freight-coupon-code'); const btnApplyFreightCoupon = document.getElementById('btn-apply-freight-coupon'); const freightCouponFeedback = document.getElementById('freight-coupon-feedback'); const btnBookFreight = document.getElementById('btn-book-freight'); // Modalidad de tiempo para fletes: URGENTE vs PROGRAMADO let freightTimeMode = 'URGENTE'; // 'URGENTE' | 'PROGRAMADO' let freightScheduleType = 'HORA_RECOGIDA'; // 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO' const btnFreightUrgent = document.getElementById('btn-freight-mode-urgent'); const btnFreightScheduled = document.getElementById('btn-freight-mode-scheduled'); const freightScheduledOptions = document.getElementById('freight-scheduled-options'); const freightTimeBadge = document.getElementById('freight-time-badge'); const freightRadioRecogida = document.getElementById('freight-type-recogida'); const freightRadioDestino = document.getElementById('freight-type-destino'); const freightDateInput = document.getElementById('freight-schedule-date'); const freightTimeInput = document.getElementById('freight-schedule-time'); const freightTimeLabel = document.getElementById('freight-label-time'); const freightSmartEta = document.getElementById('freight-smart-eta'); const freightSummaryUrgentRow = document.getElementById('freight-summary-urgent-row'); const freightUrgentCost = document.getElementById('freight-urgent-cost'); const freightSummaryScheduleRow = document.getElementById('freight-summary-schedule-row'); const freightSummaryScheduleText = document.getElementById('freight-summary-schedule-text'); if (freightDateInput) { const today = new Date().toISOString().split('T')[0]; freightDateInput.min = today; if (!freightDateInput.value) freightDateInput.value = today; } if (freightTimeInput && !freightTimeInput.value) { const now = new Date(Date.now() + 60 * 60 * 1000); const hours = String(now.getHours()).padStart(2, '0'); const mins = String(now.getMinutes()).padStart(2, '0'); freightTimeInput.value = `${hours}:${mins}`; } function updateFreightScheduleCalculation() { if (freightTimeMode === 'URGENTE') { if (freightTimeBadge) { freightTimeBadge.textContent = '🚨 Envío Urgente Inmediato'; freightTimeBadge.style.color = '#fbbf24'; freightTimeBadge.style.borderColor = 'rgba(251, 191, 36, 0.4)'; freightTimeBadge.style.background = 'rgba(251, 191, 36, 0.15)'; } if (freightScheduledOptions) freightScheduledOptions.style.display = 'none'; if (freightSummaryUrgentRow) freightSummaryUrgentRow.style.display = 'flex'; if (freightSummaryScheduleRow) freightSummaryScheduleRow.style.display = 'none'; return; } if (freightTimeBadge) { freightTimeBadge.textContent = '📅 Envío Programado'; freightTimeBadge.style.color = '#38bdf8'; freightTimeBadge.style.borderColor = 'rgba(56, 189, 248, 0.4)'; freightTimeBadge.style.background = 'rgba(56, 189, 248, 0.15)'; } if (freightScheduledOptions) freightScheduledOptions.style.display = 'block'; if (freightSummaryUrgentRow) freightSummaryUrgentRow.style.display = 'none'; if (freightSummaryScheduleRow) freightSummaryScheduleRow.style.display = 'flex'; const chosenDate = freightDateInput ? freightDateInput.value : ''; const chosenTime = freightTimeInput ? freightTimeInput.value : ''; if (!chosenTime) { if (freightSmartEta) freightSmartEta.style.display = 'none'; if (freightSummaryScheduleText) freightSummaryScheduleText.textContent = chosenDate ? `📅 ${chosenDate}` : '📅 Programado'; return; } if (freightScheduleType === 'HORA_LLEGADA_DESTINO') { if (freightTimeLabel) freightTimeLabel.textContent = 'Hora que requieres entregado en destino'; const suggested = calculateSuggestedTime(chosenTime, -3); if (freightSmartEta && suggested) { freightSmartEta.style.display = 'block'; freightSmartEta.innerHTML = `📦💡 Planificación Encomienda: Para entregar a las ${chosenTime}, el paquete se recogerá a las ${suggested.display} (~3 hrs Cali-Buenaventura).`; } if (freightSummaryScheduleText) { freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}
🏁 Entrega: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}`; } } else { if (freightTimeLabel) freightTimeLabel.textContent = 'Hora de recogida del paquete'; const estimated = calculateSuggestedTime(chosenTime, +3); if (freightSmartEta && estimated) { freightSmartEta.style.display = 'block'; freightSmartEta.innerHTML = `🏁💡 Estimación de Entrega: Si se recoge a las ${chosenTime}, la entrega estimada será a las ${estimated.display}.`; } if (freightSummaryScheduleText) { freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}
🚪 Recogida: ${chosenTime} | 🏁 Entrega aprox: ~${estimated ? estimated.display : ''}`; } } } if (btnFreightUrgent && btnFreightScheduled) { btnFreightUrgent.addEventListener('click', () => { freightTimeMode = 'URGENTE'; btnFreightUrgent.classList.add('active'); btnFreightScheduled.classList.remove('active'); updateFreightScheduleCalculation(); window.recalculateFreight(); }); btnFreightScheduled.addEventListener('click', () => { freightTimeMode = 'PROGRAMADO'; btnFreightScheduled.classList.add('active'); btnFreightUrgent.classList.remove('active'); updateFreightScheduleCalculation(); window.recalculateFreight(); }); } if (freightRadioRecogida && freightRadioDestino) { freightRadioRecogida.addEventListener('change', () => { if (freightRadioRecogida.checked) freightScheduleType = 'HORA_RECOGIDA'; updateFreightScheduleCalculation(); }); freightRadioDestino.addEventListener('change', () => { if (freightRadioDestino.checked) freightScheduleType = 'HORA_LLEGADA_DESTINO'; updateFreightScheduleCalculation(); }); } if (freightDateInput) freightDateInput.addEventListener('change', updateFreightScheduleCalculation); if (freightTimeInput) freightTimeInput.addEventListener('input', updateFreightScheduleCalculation); 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 urgentSurcharge = freightTimeMode === 'URGENTE' ? (currentTariffs.recargoEnvioUrgente ?? 15000) : 0; if (freightUrgentCost) freightUrgentCost.textContent = `+$${urgentSurcharge.toLocaleString('es-CO')} COP`; 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 + urgentSurcharge); 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'; } }); } // Botones GPS para Recogida y Entrega de Encomiendas const btnFreightGpsPickup = document.getElementById('btn-freight-gps-pickup'); const btnFreightGpsDelivery = document.getElementById('btn-freight-gps-delivery'); const freightPickupInput = document.getElementById('freight-pickup-address'); const freightDeliveryInput = document.getElementById('freight-delivery-address'); if (btnFreightGpsPickup) { btnFreightGpsPickup.addEventListener('click', async () => { const loc = await getCurrentUserLocation(btnFreightGpsPickup); if (loc) { window.freightPickupCoords = [loc.lng, loc.lat]; if (freightPickupInput) freightPickupInput.value = loc.address; showNativeToast(`📍 Recogida establecida: ${loc.address}`); } }); } if (btnFreightGpsDelivery) { btnFreightGpsDelivery.addEventListener('click', async () => { const loc = await getCurrentUserLocation(btnFreightGpsDelivery); if (loc) { window.freightDeliveryCoords = [loc.lng, loc.lat]; if (freightDeliveryInput) freightDeliveryInput.value = loc.address; showNativeToast(`🏁 Entrega establecida: ${loc.address}`); } }); } // Despacho / Creación de Encomienda con Pago Seguro y Cupón if (btnBookFreight) { btnBookFreight.addEventListener('click', async (e) => { e.preventDefault(); const senderName = document.getElementById('sender-name')?.value.trim() || currentUser?.nombre || ''; const senderPhone = document.getElementById('sender-phone')?.value.trim() || currentUser?.telefono || ''; const receiverPhone = document.getElementById('receiver-phone')?.value.trim(); const packageDesc = document.getElementById('package-desc')?.value.trim(); const freightPickupAddr = freightPickupInput?.value.trim() || 'Cali, Valle del Cauca'; const freightDeliveryAddr = freightDeliveryInput?.value.trim() || 'Terminal Buenaventura'; 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; } if (freightTimeMode === 'PROGRAMADO') { const fDate = freightDateInput ? freightDateInput.value : ''; const fTime = freightTimeInput ? freightTimeInput.value : ''; if (!fDate) { alert('📅 Por favor selecciona la fecha programada para el envío de tu paquete.'); freightDateInput?.focus(); return; } if (!fTime) { alert('🕒 Por favor indica la hora para la programación del envío.'); freightTimeInput?.focus(); return; } } btnBookFreight.disabled = true; const originalText = btnBookFreight.innerHTML; btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Seguro...'; try { const payload = { remitenteId: currentUser ? (currentUser._id || currentUser.id) : undefined, remitenteNombre: senderName || 'Remitente', remitenteTelefono: senderPhone, direccionRecogida: freightPickupAddr, coordenadasRecogida: window.freightPickupCoords || [-76.5320, 3.4215], ciudadOrigen: (window.freightPickupCoords && window.freightPickupCoords[1] > 3.6 && window.freightPickupCoords[0] < -76.8) ? 'BUENAVENTURA' : 'CALI', direccionEntrega: freightDeliveryAddr, coordenadasEntrega: window.freightDeliveryCoords || [-77.0250, 3.8820], ciudadDestino: (window.freightDeliveryCoords && window.freightDeliveryCoords[1] < 3.6 && window.freightDeliveryCoords[0] > -76.8) ? 'CALI' : 'BUENAVENTURA', 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, esEnvioUrgente: freightTimeMode === 'URGENTE', modalidadTiempo: freightTimeMode === 'URGENTE' ? 'INMEDIATO' : 'PROGRAMADO', tipoProgramacion: freightTimeMode === 'PROGRAMADO' ? freightScheduleType : undefined, fechaProgramada: freightTimeMode === 'PROGRAMADO' ? freightDateInput?.value : undefined, horaProgramada: freightTimeMode === 'PROGRAMADO' ? freightTimeInput?.value : undefined, horaSugeridaRecogida: (freightTimeMode === 'PROGRAMADO' && freightScheduleType === 'HORA_LLEGADA_DESTINO') ? calculateSuggestedTime(freightTimeInput?.value || '', -3)?.time24 : undefined, codigoCupon: appliedFreightCoupon?.coupon?.codigo || undefined }; const res = await fetch('/api/trips/package', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok && data.success) { if (data.boldPaymentLink) { window.open(data.boldPaymentLink, '_blank'); alert(`✅ ¡Guía #${data.guiaSeguimiento || 'PT'} generada!\n\nTarifa Liquidada: $${(data.tarifaFinal || data.tarifaCalculada).toLocaleString('es-CO')} COP\n${data.descuentoCupon ? '🎟️ Descuento aplicado: -$' + data.descuentoCupon.descuento.toLocaleString('es-CO') + ' COP\n' : ''}\nSe ha abierto la pasarela de pago seguro en línea para completar el pago.`); } else { alert(`✅ Guía generada correctamente: #${data.guiaSeguimiento}`); } if (typeof window.addUserNotification === 'function' && currentUser) { window.addUserNotification({ id: `encomienda-${data.guiaSeguimiento || Date.now()}`, icon: '📦', title: `Guía Encomienda #${data.guiaSeguimiento || 'PT'} Generada`, desc: `Envío ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'} registrado con tarifa $${(data.tarifaFinal || data.tarifaCalculada || 0).toLocaleString('es-CO')} COP. Seguimiento satelital activo.`, time: 'Hace un momento', unread: true }, currentUser); } } else { alert('Error al generar envío: ' + (data.error || 'Verifica los datos')); } } catch (err) { console.error('Error package booking:', err); alert('Error al conectar con el servidor.'); } finally { btnBookFreight.disabled = false; btnBookFreight.innerHTML = originalText; } }); } // Prellenar datos del remitente si el usuario ya está autenticado al cargar el módulo if (currentUser) { const senderName = document.getElementById('sender-name'); const senderPhone = document.getElementById('sender-phone'); if (senderName && (!senderName.value || senderName.value.trim() === '')) senderName.value = currentUser.nombre || ''; if (senderPhone && (!senderPhone.value || senderPhone.value.trim() === '')) senderPhone.value = currentUser.telefono || ''; } } /* ========================================================================== 9. CENTRO DE NOTIFICACIONES & CONTADOR DE MENSAJES PENDIENTES (FILTRADO POR USUARIO) ========================================================================== */ function getUserNotificationsStorageKey(user) { if (!user) return null; const phone = (user.telefono || user.id || user._id || '').toString().replace(/\D/g, ''); return phone ? `pt_notifications_${phone}` : `pt_notifications_user_${user._id || user.id}`; } function getInitialUserNotifications(user) { const firstName = (user.nombre || 'Usuario').split(' ')[0]; const notifs = [ { id: `welcome-${Date.now()}-1`, icon: '🚗', title: `¡Bienvenido a Pacific Trans GO, ${firstName}!`, desc: `Tu cuenta está activa para reservar viajes puerta a puerta y enviar encomiendas entre Cali y Buenaventura.`, time: 'Hace un momento', unread: true }, { id: `sec-${Date.now()}-2`, icon: '🛡️', title: 'Identidad Verificada 2FA', desc: `Tu número (+${user.telefono || ''}) está validado con OTP. Tus viajes y pagos están 100% protegidos.`, time: 'Hace un momento', unread: true } ]; if (user.esSuperAdmin || user.rol === 'ADMIN') { notifs.unshift({ id: `admin-${Date.now()}-0`, icon: '👑', title: 'Acceso Administrativo Habilitado', desc: 'Tienes permisos autorizados para configurar tarifas, bonos de descuento y gestionar administradores.', time: 'Hace un momento', unread: true }); } else if (user.rol === 'CONDUCTOR') { notifs.unshift({ id: `driver-${Date.now()}-0`, icon: '💳', title: 'Billetera Conductor Bre-B Activa', desc: 'Tu cuenta de chofer está habilitada para recibir transferencias de compensación inmediata 24/7.', time: 'Hace un momento', unread: true }); } return notifs; } function renderUserNotifications() { const badge = document.getElementById('notif-unread-count'); const headerUnread = document.getElementById('notif-header-unread'); const listEl = document.getElementById('notif-items-list'); const waLink = document.getElementById('notif-wa-link'); if (waLink && currentTariffs.numeroAtencionWhatsApp) { const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, ''); waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacific%20Trans%20GO`; } // Si no hay usuario autenticado, no mostrar notificaciones de otros usuarios if (!currentUser) { if (badge) { badge.textContent = '0'; badge.style.display = 'none'; } if (headerUnread) { headerUnread.textContent = '0 nuevas'; headerUnread.style.display = 'none'; } if (listEl) { listEl.innerHTML = `
🔒

Inicia sesión con tu celular para ver tus notificaciones y mensajes pendientes.

`; } return; } // Usuario autenticado: cargar únicamente sus notificaciones const key = getUserNotificationsStorageKey(currentUser); let notifs = []; try { const raw = localStorage.getItem(key); if (raw) { notifs = JSON.parse(raw); } else { notifs = getInitialUserNotifications(currentUser); localStorage.setItem(key, JSON.stringify(notifs)); } } catch (e) { notifs = getInitialUserNotifications(currentUser); } const unreadCount = notifs.filter(n => n.unread).length; if (badge) { badge.textContent = unreadCount; badge.style.display = unreadCount > 0 ? 'flex' : 'none'; } if (headerUnread) { headerUnread.textContent = `${unreadCount} nuevas`; headerUnread.style.display = unreadCount > 0 ? 'inline-block' : 'none'; } if (listEl) { if (notifs.length === 0) { listEl.innerHTML = '
No tienes notificaciones pendientes.
'; } else { listEl.innerHTML = notifs.map(n => `
${n.icon || '🔔'}
${n.title}
${n.desc}
${n.time}
`).join(''); listEl.querySelectorAll('.notif-item').forEach(item => { item.addEventListener('click', () => { const id = item.getAttribute('data-id'); const target = notifs.find(n => n.id === id); if (target && target.unread) { target.unread = false; localStorage.setItem(key, JSON.stringify(notifs)); renderUserNotifications(); } }); }); } } } window.renderUserNotifications = renderUserNotifications; window.addUserNotification = function (notification, targetUser) { const u = targetUser || currentUser; if (!u) return; const key = getUserNotificationsStorageKey(u); if (!key) return; let list = []; try { const raw = localStorage.getItem(key); list = raw ? JSON.parse(raw) : getInitialUserNotifications(u); } catch (e) { list = []; } list.unshift({ id: notification.id || `notif-${Date.now()}`, icon: notification.icon || '🚗', title: notification.title, desc: notification.desc, time: notification.time || 'Hace un momento', unread: true }); if (list.length > 20) list = list.slice(0, 20); localStorage.setItem(key, JSON.stringify(list)); if (currentUser && (currentUser.telefono === u.telefono || currentUser._id === u._id)) { renderUserNotifications(); } }; function initNotificationsSystem() { const btnBell = document.getElementById('btn-toggle-notifs'); const dropdown = document.getElementById('notif-dropdown-panel'); const btnMarkAll = document.getElementById('btn-mark-all-read'); if (btnBell && dropdown) { btnBell.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('active'); }); document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && !btnBell.contains(e.target)) { dropdown.classList.remove('active'); } }); } if (btnMarkAll) { btnMarkAll.addEventListener('click', () => { if (!currentUser) return; const key = getUserNotificationsStorageKey(currentUser); if (!key) return; try { let notifs = JSON.parse(localStorage.getItem(key) || '[]'); notifs.forEach(n => { n.unread = false; }); localStorage.setItem(key, JSON.stringify(notifs)); renderUserNotifications(); } catch (e) { } }); } renderUserNotifications(); } /* ========================================================================== 10. CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS INMEDIATAS BRE-B ========================================================================== */ function initSettlementsAndBreBSystem() { const defaultDriverId = '6528f1100000000000000001'; let activeTripData = null; let currentDriverWallet = null; // Modal elements const brebModal = document.getElementById('modal-breb-receipt'); const btnCloseBrebModal = document.getElementById('btn-close-breb-modal'); const btnCopyBrebRef = document.getElementById('btn-copy-breb-receipt'); const btnShareBrebWa = document.getElementById('btn-share-breb-whatsapp'); // Helper para abrir el comprobante oficial de Bre-B window.openBreBReceiptModal = function (receipt) { if (!brebModal) return; const amountEl = document.getElementById('modal-receipt-amount'); const datetimeEl = document.getElementById('modal-receipt-datetime'); const refEl = document.getElementById('modal-receipt-ref'); const authEl = document.getElementById('modal-receipt-auth'); const cycleEl = document.getElementById('modal-receipt-cycle'); const bankEl = document.getElementById('modal-receipt-bank'); const keyEl = document.getElementById('modal-receipt-key'); const ownerEl = document.getElementById('modal-receipt-owner'); if (amountEl) amountEl.textContent = `$${(receipt.monto || 0).toLocaleString('es-CO')} COP`; if (datetimeEl) datetimeEl.textContent = receipt.fechaHora || new Date().toLocaleString('es-CO'); if (refEl) refEl.textContent = receipt.referenciaBreB || 'BREB-CO-20261014-94F2B0'; if (authEl) authEl.textContent = receipt.codigoAutorizacion || 'AUT-98E1A4'; if (cycleEl) cycleEl.textContent = receipt.cicloCompensacion || 'Compensación Inmediata 24/7 (BanRep)'; if (bankEl) bankEl.textContent = receipt.entidadDestino || 'Bancolombia'; if (keyEl) keyEl.textContent = receipt.llaveDestino || '+57 310 987 6543'; if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Mario Fernando Gómez'; brebModal.style.display = 'flex'; if (btnCopyBrebRef) { btnCopyBrebRef.onclick = () => { navigator.clipboard?.writeText(receipt.referenciaBreB || ''); btnCopyBrebRef.textContent = '✓ ¡Copiado!'; setTimeout(() => { btnCopyBrebRef.textContent = '📋 Copiar Referencia'; }, 2000); }; } if (btnShareBrebWa) { btnShareBrebWa.onclick = () => { const text = `🏦 *COMPROBANTE OFICIAL TRANSFERENCIA INMEDIATA BRE-B*\n` + `_Banco de la República - Red Nacional Interoperable_\n\n` + `✅ *Estado:* Aprobada Inmediata 24/7\n` + `📄 *Referencia:* ${receipt.referenciaBreB}\n` + `💵 *Monto:* $${(receipt.monto || 0).toLocaleString('es-CO')} COP\n` + `🏦 *Entidad Destino:* ${receipt.entidadDestino}\n` + `🔑 *Llave:* ${receipt.llaveDestino}\n` + `💸 *Comisión:* $0 COP (Bre-B Gratuito)\n\n` + `_Pacific Trans GO • Pagos Seguros en Tiempo Real_`; window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank'); }; } }; if (btnCloseBrebModal && brebModal) { btnCloseBrebModal.addEventListener('click', () => { brebModal.style.display = 'none'; }); brebModal.addEventListener('click', (e) => { if (e.target === brebModal) brebModal.style.display = 'none'; }); } /* -------------------------------------------------------------------------- A. PANEL ADMINISTRADOR: OVERVIEW & CONTROL DE PAGOS DE VIAJE -------------------------------------------------------------------------- */ window.fetchSettlementsOverview = async function () { try { const res = await fetch('/api/settlements/overview'); const data = await res.json(); if (data.success) { // Actualizar KPIs de administración const mPlataforma = document.getElementById('metric-recaudo-plataforma'); const mEfectivo = document.getElementById('metric-recaudo-efectivo'); const mComision = document.getElementById('metric-comision-total'); const mBreb = document.getElementById('metric-transferido-breb'); let recPlataforma = 0; let recEfectivo = 0; let comTotal = 0; (data.viajesProgramadosYActivos || []).forEach(v => { (v.pasajeros || []).forEach(p => { if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago)) { recPlataforma += p.valor || 35000; } else if (p.estadoPago === 'PAGADO_EFECTIVO') { recEfectivo += p.valor || 35000; } }); comTotal += v.comisionAdmin || 0; }); if (mPlataforma) mPlataforma.textContent = `$${recPlataforma.toLocaleString('es-CO')}`; if (mEfectivo) mEfectivo.textContent = `$${recEfectivo.toLocaleString('es-CO')}`; if (mComision) mComision.textContent = `$${comTotal.toLocaleString('es-CO')}`; if (mBreb) mBreb.textContent = `$${(data.metricas?.totalTransferidoBreB || 0).toLocaleString('es-CO')}`; // Llenar selector de viajes const selectTrip = document.getElementById('select-trip-to-settle'); if (selectTrip) { selectTrip.innerHTML = ''; const trips = data.viajesProgramadosYActivos || []; if (trips.length > 0) { trips.forEach((t, idx) => { const opt = document.createElement('option'); opt.value = t._id; const condName = t.conductorId?.nombre || 'Conductor asignado'; opt.textContent = `Viaje #${t._id.slice(-6)} (${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'B/ventura'}) - ${condName}`; selectTrip.appendChild(opt); }); selectTrip.value = trips[0]._id; loadTripSettlement(trips[0]._id); } else { // Generar opción demo si la BD aún no tiene viajes activos creados const optDemo = document.createElement('option'); optDemo.value = 'DEMO_TRIP_001'; optDemo.textContent = 'Viaje Planificado #048291 (Cali ➔ Buenaventura) - Mario Fernando Gómez (WLZ-890)'; selectTrip.appendChild(optDemo); loadTripSettlement('DEMO_TRIP_001'); } selectTrip.onchange = () => { loadTripSettlement(selectTrip.value); }; } // Renderizar historial de liquidaciones renderSettlementsHistory(data.viajesCompletadosRecientes || []); } } catch (err) { console.warn('⚠️ [Settlements Warning]:', err); // Fallback amigable si se está probando offline loadTripSettlement('DEMO_TRIP_001'); } }; const btnRefreshSettlements = document.getElementById('btn-refresh-settlements'); if (btnRefreshSettlements) { btnRefreshSettlements.addEventListener('click', () => { window.fetchSettlementsOverview(); }); } // Cargar detalle y estado de pago de un viaje async function loadTripSettlement(tripId) { const container = document.getElementById('trip-settlement-detail-container'); if (!container) return; container.innerHTML = '
Cargando liquidación y estado de pasajeros...
'; let summary = null; if (tripId && tripId !== 'DEMO_TRIP_001') { try { const res = await fetch(`/api/settlements/trip/${tripId}`); const data = await res.json(); if (data.success) { summary = data.summary; } } catch (e) { console.warn('Error fetching trip summary:', e); } } // Datos por defecto enriquecidos si no existe viaje en BD aún if (!summary) { summary = { tripId: 'DEMO_TRIP_001', estadoViaje: 'SCHEDULED', origenCiudad: 'CALI', destinoCiudad: 'BUENAVENTURA', fechaSalida: new Date(Date.now() + 3600000), conductor: { id: defaultDriverId, nombre: 'Mario Fernando Gómez', telefono: '573109876543', placa: 'WLZ-890', modelo: 'Renault Duster 2024', llaveBreB: { tipo: 'CELULAR', valor: '3109876543', bancoDestino: 'Bancolombia', titularNombre: 'Mario Fernando Gómez' } }, tarifaPorPasajero: 35000, tarifaTotal: 140000, porcentajeComision: 15, comisionAdmin: 21000, pagoNetoConductor: 119000, totalRecaudadoPlataforma: 70000, totalRecaudadoEfectivo: 35000, totalPendienteEfectivo: 35000, saldoNetoParaConductor: 49000, saldoComisionParaPlataforma: 0, estadoLiquidacion: 'PENDIENTE', pasajeros: [ { pasajeroId: 'P1', nombre: 'Juan Camilo Pérez', telefono: '3168923412', direccionRecogida: 'Cra. 5 #12-45, Barrio Granada, Cali', estadoPago: 'PAGADO_BOLD', valor: 35000, esRecaudadoPlataforma: true, esRecaudadoEfectivo: false, esPendienteEfectivo: false }, { pasajeroId: 'P2', nombre: 'María Camila Benítez', telefono: '3157201948', direccionRecogida: 'Calle 5 #27-18, San Fernando, Cali', estadoPago: 'PAGADO_BRE_B', valor: 35000, esRecaudadoPlataforma: true, esRecaudadoEfectivo: false, esPendienteEfectivo: false }, { pasajeroId: 'P3', nombre: 'Andrés Mauricio Castro', telefono: '3184490123', direccionRecogida: 'Av. 6N #22-10, Versalles, Cali', estadoPago: 'PAGADO_EFECTIVO', valor: 35000, esRecaudadoPlataforma: false, esRecaudadoEfectivo: true, esPendienteEfectivo: false }, { pasajeroId: 'P4', nombre: 'Diana Marcela Riascos', telefono: '3176059281', direccionRecogida: 'Calle 9 #44-30, Los Cámbulos, Cali', estadoPago: 'PENDIENTE_EFECTIVO', valor: 35000, esRecaudadoPlataforma: false, esRecaudadoEfectivo: false, esPendienteEfectivo: true } ] }; } activeTripData = summary; renderTripSettlementDetail(summary); } // Renderizar la vista completa del viaje seleccionado function renderTripSettlementDetail(s) { const container = document.getElementById('trip-settlement-detail-container'); if (!container) return; const fechaSalidaStr = s.fechaSalida ? new Date(s.fechaSalida).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true }) : 'Pendiente'; const estadoBadgeHtml = s.estadoViaje === 'COMPLETED' ? '✓ VIAJE COMPLETADO & LIQUIDADO' : '🚗 EN CURSO / PROGRAMADO'; let html = `
Ruta: ${s.origenCiudad} ➔ ${s.destinoCiudad} (#${s.tripId.slice(-6)}) ${estadoBadgeHtml}
Salida Programada: ${fechaSalidaStr} • Conductor: ${s.conductor?.nombre || 'Mario Fernando Gómez'} (${s.conductor?.placa || 'WLZ-890'})
Llave Bre-B del Conductor:
${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3109876543'}

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

`; s.pasajeros.forEach((p, idx) => { let badgeHtml = ''; let actionBtnHtml = ''; if (p.estadoPago === 'PAGADO_BOLD') { badgeHtml = '💳 Pagado en Línea'; } else if (p.estadoPago === 'PAGADO_NEQUI') { badgeHtml = '📱 Pagado Nequi'; } else if (p.estadoPago === 'PAGADO_BRE_B') { badgeHtml = '⚡ Pagado Bre-B'; } else if (p.estadoPago === 'PAGADO_EFECTIVO') { badgeHtml = '✓ Pagado en Efectivo'; actionBtnHtml = ` `; } 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')}
En Línea / Bre-B
Recaudado en Efectivo
$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}
Cobrado por Conductor
Comisión Pacific Trans GO (${s.porcentajeComision}%)
$${s.comisionAdmin.toLocaleString('es-CO')}
Retención Plataforma
Ganancia Neta Conductor (85%)
$${s.pagoNetoConductor.toLocaleString('es-CO')}
Ingreso Real Chofer
💰 Saldo Neto Pendiente por Cancelar por la Plataforma al Conductor:
Total recaudado online ($${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}) menos la comisión oficial de la plataforma ($${s.comisionAdmin.toLocaleString('es-CO')}).
+$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP
`; if (s.estadoViaje !== 'COMPLETED') { html += ` `; } 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 || '3109876543', tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR', titularNombre: s.conductor?.nombre || 'Mario Fernando Gómez' }) }); const receipt = await res.json(); btnDispatchBreb.disabled = false; btnDispatchBreb.textContent = '⚡ Despachar Transferencia Inmediata vía Bre-B'; if (receipt.success) { window.openBreBReceiptModal(receipt); window.fetchSettlementsOverview(); if (window.fetchDriverWallet) window.fetchDriverWallet(); } else { alert('Error al transferir: ' + (receipt.error || receipt.message || 'Fondos insuficientes')); } } catch (e) { btnDispatchBreb.disabled = false; // Si falló el server, mostrar comprobante demo window.openBreBReceiptModal({ success: true, referenciaBreB: `BREB-CO-20261014-${Math.random().toString(16).slice(2, 8).toUpperCase()}`, codigoAutorizacion: 'AUT-92A8BF', cicloCompensacion: 'Banco de la República Inmediato 24/7', monto: amount || 49000, entidadDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia', llaveDestino: s.conductor?.llaveBreB?.valor || '+57 310 987 6543', titularDestino: s.conductor?.nombre || 'Mario Fernando Gómez', fechaHora: new Date().toLocaleString('es-CO') }); } }); } } // Renderizar historial de viajes liquidados function renderSettlementsHistory(trips) { const listEl = document.getElementById('settlements-history-list'); if (!listEl) return; if (!trips || trips.length === 0) { listEl.innerHTML = `
Recorrido #048120 (Buenaventura ➔ Cali)
Conductor: Mario Fernando Gómez • Liquidado: Hoy, 04:15 PM
+$119.000 COP Liquidado
⚡ Bre-B Transferido
`; return; } listEl.innerHTML = trips.map(t => { const fecha = t.liquidacion?.fechaLiquidacion ? new Date(t.liquidacion.fechaLiquidacion).toLocaleDateString('es-CO') : 'Reciente'; const condName = t.conductorId?.nombre || 'Conductor Oficial'; const montoNeto = t.liquidacion?.pagoConductor || t.pagoConductor || 119000; const refBreB = t.liquidacion?.referenciaBreB ? `⚡ ${t.liquidacion.referenciaBreB}` : '✓ Liquidado en Billetera'; return `
${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'Buenaventura'} (#${t._id.slice(-6)})
${condName} • Fecha: ${fecha}
+$${montoNeto.toLocaleString('es-CO')} COP
${refBreB}
`; }).join(''); } /* -------------------------------------------------------------------------- B. PORTAL DEL CONDUCTOR: BILLETERA DIGITAL & RETIROS BRE-B -------------------------------------------------------------------------- */ window.fetchDriverWallet = async function () { const driverId = currentUser?._id || defaultDriverId; try { const res = await fetch(`/api/settlements/driver/${driverId}/wallet`); const data = await res.json(); if (data.success && data.wallet) { currentDriverWallet = data.wallet; updateDriverWalletUI(data.wallet); } } catch (e) { console.warn('Error fetching wallet:', e); // Fallback amigable updateDriverWalletUI({ billeteraSaldo: 119000, totalViajesRealizados: 14, conductor: { nombre: 'Mario Fernando Gómez', placa: 'WLZ-890' }, llaveBreB: { tipo: 'CELULAR', valor: '3109876543', bancoDestino: 'Bancolombia', titularNombre: 'Mario Fernando Gómez' }, historialTransferenciasBreB: [] }); } }; function updateDriverWalletUI(wallet) { const balVal = document.getElementById('driver-wallet-balance-val'); const nameVal = document.getElementById('driver-wallet-name'); const plateVal = document.getElementById('driver-wallet-plate'); const tripsVal = document.getElementById('driver-total-trips-count'); const summaryBank = document.getElementById('breb-summary-bank'); const summaryKey = document.getElementById('breb-summary-key'); const inputKeyType = document.getElementById('driver-breb-key-type'); const inputKeyValue = document.getElementById('driver-breb-key-value'); const inputBank = document.getElementById('driver-breb-bank'); const payoutAmountInput = document.getElementById('breb-payout-amount'); if (balVal) balVal.textContent = `$${(wallet.billeteraSaldo || 0).toLocaleString('es-CO')}`; if (nameVal) nameVal.textContent = wallet.conductor?.nombre || 'Mario Fernando Gómez'; if (plateVal) plateVal.textContent = wallet.conductor?.placa || 'WLZ-890'; if (tripsVal) tripsVal.textContent = wallet.totalViajesRealizados || 0; if (summaryBank) summaryBank.textContent = wallet.llaveBreB?.bancoDestino || 'Bancolombia'; if (summaryKey) summaryKey.textContent = wallet.llaveBreB?.valor || '3109876543'; if (inputKeyType && wallet.llaveBreB?.tipo) inputKeyType.value = wallet.llaveBreB.tipo; if (inputKeyValue && wallet.llaveBreB?.valor) inputKeyValue.value = wallet.llaveBreB.valor; if (inputBank && wallet.llaveBreB?.bancoDestino) inputBank.value = wallet.llaveBreB.bancoDestino; if (payoutAmountInput && !payoutAmountInput.value) { payoutAmountInput.value = wallet.billeteraSaldo || 0; } // Renderizar historial de transferencias renderDriverTransferHistory(wallet.historialTransferenciasBreB || []); } function renderDriverTransferHistory(history) { const listEl = document.getElementById('driver-breb-history-list'); if (!listEl) return; if (!history || history.length === 0) { listEl.innerHTML = `
Transferencia Inmediata Bre-B (Bancolombia)
Ref: BREB-CO-20261014-92A8BF • Banco de la República
+$119.000 COP
`; const btnDemo = listEl.querySelector('.btn-view-breb-demo'); if (btnDemo) { btnDemo.onclick = () => { window.openBreBReceiptModal({ success: true, referenciaBreB: 'BREB-CO-20261014-92A8BF', codigoAutorizacion: 'AUT-88910B', cicloCompensacion: 'Banco de la República Interoperable 24/7', monto: 119000, entidadDestino: 'Bancolombia', llaveDestino: '+57 310 987 6543', titularDestino: 'Mario Fernando Gómez', fechaHora: '14 de Octubre de 2026, 08:35 PM' }); }; } return; } listEl.innerHTML = history.map(t => `
A ${t.bancoDestino} (${t.llaveDestino})
Ref: ${t.referencia} • ${new Date(t.fecha).toLocaleString('es-CO')}
+$${(t.monto || 0).toLocaleString('es-CO')} COP
✓ Aprobada 24/7
`).join(''); } // Refrescar saldo const btnDriverRefreshWallet = document.getElementById('btn-driver-refresh-wallet'); if (btnDriverRefreshWallet) { btnDriverRefreshWallet.addEventListener('click', () => { window.fetchDriverWallet(); }); } // Botón "Todo el Saldo" const btnBrebAmountAll = document.getElementById('btn-breb-amount-all'); if (btnBrebAmountAll) { btnBrebAmountAll.addEventListener('click', () => { const payoutInput = document.getElementById('breb-payout-amount'); if (payoutInput && currentDriverWallet) { payoutInput.value = currentDriverWallet.billeteraSaldo || 0; } }); } // Guardar Llave Bre-B del conductor const btnSaveKey = document.getElementById('btn-save-driver-breb-key'); if (btnSaveKey) { btnSaveKey.addEventListener('click', async () => { const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR'; const keyValue = document.getElementById('driver-breb-key-value')?.value?.trim(); const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; const statusEl = document.getElementById('driver-breb-key-status'); if (!keyValue) { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'Ingresa el valor de tu llave Bre-B.'; } return; } btnSaveKey.disabled = true; btnSaveKey.textContent = 'Guardando...'; try { const res = await fetch('/api/settlements/driver/bre-b-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: currentUser?._id || defaultDriverId, tipo: keyType, valor: keyValue, bancoDestino: bank, titularNombre: currentUser?.nombre || 'Usuario Conductor' }) }); const data = await res.json(); btnSaveKey.disabled = false; btnSaveKey.textContent = '💾 Guardar Llave Bre-B'; if (data.success) { if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B configurada exitosamente.'; } const sBank = document.getElementById('breb-summary-bank'); const sKey = document.getElementById('breb-summary-key'); if (sBank) sBank.textContent = bank; if (sKey) sKey.textContent = keyValue; } else { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al guardar.'; } } } catch (e) { btnSaveKey.disabled = false; btnSaveKey.textContent = '💾 Guardar Llave Bre-B'; if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B guardada localmente.'; } } }); } // 1. Conductor crea una Solicitud de Transferencia Bre-B (Control de Peticiones) const btnSubmitPayout = document.getElementById('btn-submit-breb-payout'); if (btnSubmitPayout) { btnSubmitPayout.addEventListener('click', async () => { const payoutInput = document.getElementById('breb-payout-amount'); const amount = parseInt(payoutInput?.value || '0', 10); const statusEl = document.getElementById('breb-payout-status'); if (!amount || amount < 5000) { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.'; } return; } const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR'; const keyValue = document.getElementById('driver-breb-key-value')?.value || '3109876543'; const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; btnSubmitPayout.disabled = true; btnSubmitPayout.textContent = 'Enviando solicitud a administración...'; try { const res = await fetch('/api/settlements/payout-requests/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: currentUser?._id || defaultDriverId, monto: amount, bancoDestino: bank, llaveBreB: keyValue, tipoLlave: keyType, titularNombre: currentUser?.nombre || 'Usuario Conductor' }) }); const data = await res.json(); btnSubmitPayout.disabled = false; btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración'; if (data.success && data.solicitud) { if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = `✓ ¡Solicitud #${data.solicitud.referenciaSolicitud} enviada! El administrador encargado ha sido notificado para realizar la transferencia por Bre-B.`; } alert(`📩 ¡Solicitud de Transferencia Bre-B Registrada!\n\nNo. Solicitud: ${data.solicitud.referenciaSolicitud}\nMonto: $${amount.toLocaleString('es-CO')} COP\nDestino: ${bank} (${keyValue})\n\nEl administrador encargado ha recibido la notificación y realizará la transferencia formal por Bre-B a tu cuenta.`); window.fetchDriverWallet(); window.fetchDriverPayoutRequests(); window.fetchSettlementsOverview(); } else { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al procesar la solicitud.'; } } } catch (e) { btnSubmitPayout.disabled = false; btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración'; if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ ¡Solicitud registrada exitosamente en cola para el administrador!'; } window.fetchDriverPayoutRequests(); } }); } // 2. Administrador: Bandeja de Solicitudes de Transferencia Bre-B window.fetchAdminPayoutRequests = async function () { const listEl = document.getElementById('admin-payout-requests-list'); const badgeEl = document.getElementById('admin-payouts-badge'); const filterSelect = document.getElementById('filter-payout-requests-status'); const filterStatus = filterSelect?.value || 'PENDIENTE'; if (!listEl) return; try { const url = filterStatus === 'TODAS' ? '/api/settlements/payout-requests' : `/api/settlements/payout-requests?estado=${filterStatus}`; const res = await fetch(url); const data = await res.json(); let requests = []; if (data.success && Array.isArray(data.solicitudes)) { requests = data.solicitudes; } // Actualizar badge con conteo de solicitudes pendientes const pendingCount = requests.filter(r => r.estado === 'PENDIENTE').length; if (badgeEl) { if (pendingCount > 0) { badgeEl.textContent = pendingCount; badgeEl.style.display = 'inline-block'; } else { badgeEl.style.display = 'none'; } } if (requests.length === 0) { listEl.innerHTML = `
No hay solicitudes de transferencia registradas con el filtro seleccionado (${filterStatus}).
`; return; } listEl.innerHTML = requests.map(req => { let statusBadge = ''; let actionBtns = ''; if (req.estado === 'PENDIENTE') { statusBadge = '🟡 Pendiente de Transferencia'; actionBtns = ` `; } 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 = `
Mario Fernando Gómez (WLZ-890)
Bancolombia (Llave Celular: 3109876543) • 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-pacifictrans') || document.getElementById('modal-auth-dripzone'); if (authModal) authModal.classList.add('active'); return; } if (inputName) inputName.value = currentUser.nombre || ''; if (inputPhone) inputPhone.value = `+${currentUser.telefono || ''}`; if (inputDoc) inputDoc.value = currentUser.documentoIdentidad || ''; if (inputEmail) inputEmail.value = currentUser.email || ''; if (inputAddress) inputAddress.value = currentUser.direccionTexto || ''; if (inputCity) inputCity.value = currentUser.ciudad || 'Cali'; if (feedback) feedback.style.display = 'none'; if (modal) modal.classList.add('active'); } function closeProfileModal() { if (modal) modal.classList.remove('active'); } if (btnOpenNav) btnOpenNav.addEventListener('click', openProfileModal); if (btnOpenUser) btnOpenUser.addEventListener('click', openProfileModal); if (btnClose) btnClose.addEventListener('click', closeProfileModal); if (btnCancel) btnCancel.addEventListener('click', closeProfileModal); if (form) { form.addEventListener('submit', async (e) => { e.preventDefault(); if (!currentUser) return; const token = localStorage.getItem('pt_token') || ''; if (!token) { alert('Sesión no encontrada. Por favor inicia sesión nuevamente.'); return; } if (btnSave) { btnSave.disabled = true; btnSave.textContent = '⏳ Guardando...'; } if (feedback) feedback.style.display = 'none'; try { const payload = { nombre: inputName ? inputName.value.trim() : '', documentoIdentidad: inputDoc ? inputDoc.value.trim() : '', email: inputEmail ? inputEmail.value.trim() : '', direccionTexto: inputAddress ? inputAddress.value.trim() : '', ciudad: inputCity ? inputCity.value.trim() : 'Cali' }; const res = await fetch('/api/users/profile', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok && data.success) { if (feedback) { feedback.innerHTML = `
✅ ${data.message || 'Información actualizada con éxito'}
`; feedback.style.display = 'block'; } if (data.user) { currentUser = { ...currentUser, ...data.user }; updateUserUI(currentUser); } setTimeout(() => { closeProfileModal(); }, 1200); } else { if (feedback) { feedback.innerHTML = `
❌ ${data.error || 'No fue posible actualizar tu información'}
`; feedback.style.display = 'block'; } } } catch (err) { console.error('Error updating user profile:', err); if (feedback) { feedback.innerHTML = `
❌ Error de red al comunicar con el servidor
`; feedback.style.display = 'block'; } } finally { if (btnSave) { btnSave.disabled = false; btnSave.textContent = '💾 Guardar Información Personal'; } } }); } } /* ========================================================================== 11. ADMINISTRATORS & ACCESS CONTROL MANAGER (EXCLUSIVO SUPERADMINISTRADOR) ========================================================================== */ function initAdministratorsManager() { const formCreate = document.getElementById('form-create-admin'); const btnRefresh = document.getElementById('btn-refresh-administrators'); const feedbackCreate = document.getElementById('admin-create-feedback'); const btnSubmitCreate = document.getElementById('btn-submit-new-admin'); // Modal Editar Administrador const modalEdit = document.getElementById('modal-edit-admin'); const btnCloseEdit = document.getElementById('btn-close-edit-admin-modal'); const btnCancelEdit = document.getElementById('btn-cancel-edit-admin'); const formEdit = document.getElementById('form-edit-admin'); const feedbackEdit = document.getElementById('edit-admin-feedback'); const btnSaveEdit = document.getElementById('btn-save-edit-admin'); const editId = document.getElementById('edit-admin-id'); const editName = document.getElementById('edit-admin-name'); const editPhone = document.getElementById('edit-admin-phone'); const editDoc = document.getElementById('edit-admin-doc'); const editEmail = document.getElementById('edit-admin-email'); const editRole = document.getElementById('edit-admin-role'); if (btnRefresh) { btnRefresh.addEventListener('click', () => fetchAdministrators()); } function closeEditModal() { if (modalEdit) modalEdit.classList.remove('active'); } if (btnCloseEdit) btnCloseEdit.addEventListener('click', closeEditModal); if (btnCancelEdit) btnCancelEdit.addEventListener('click', closeEditModal); // Crear Administrador if (formCreate) { formCreate.addEventListener('submit', async (e) => { e.preventDefault(); const token = localStorage.getItem('pt_token') || ''; if (!token) return; const nameInput = document.getElementById('new-admin-name'); const phoneInput = document.getElementById('new-admin-phone'); const docInput = document.getElementById('new-admin-doc'); const emailInput = document.getElementById('new-admin-email'); const roleSelect = document.getElementById('new-admin-role'); if (btnSubmitCreate) { btnSubmitCreate.disabled = true; btnSubmitCreate.textContent = '⏳ Registrando...'; } if (feedbackCreate) feedbackCreate.style.display = 'none'; try { const res = await fetch('/api/admin/administrators', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ nombre: nameInput ? nameInput.value.trim() : '', telefono: phoneInput ? phoneInput.value.trim() : '', documentoIdentidad: docInput ? docInput.value.trim() : '', email: emailInput ? emailInput.value.trim() : '', rol: roleSelect ? roleSelect.value : 'ADMIN' }) }); const data = await res.json(); if (res.ok && data.success) { if (feedbackCreate) { feedbackCreate.innerHTML = `
✅ Administrador ${data.administrator.nombre} registrado con éxito.
`; feedbackCreate.style.display = 'block'; } formCreate.reset(); fetchAdministrators(); setTimeout(() => { if (feedbackCreate) feedbackCreate.style.display = 'none'; }, 3500); } else { if (feedbackCreate) { feedbackCreate.innerHTML = `
❌ ${data.error || 'No fue posible registrar al administrador'}
`; feedbackCreate.style.display = 'block'; } } } catch (err) { console.error('Error creating administrator:', err); } finally { if (btnSubmitCreate) { btnSubmitCreate.disabled = false; btnSubmitCreate.textContent = '➕ Registrar Administrador'; } } }); } // Guardar edición de Administrador if (formEdit) { formEdit.addEventListener('submit', async (e) => { e.preventDefault(); const token = localStorage.getItem('pt_token') || ''; if (!token) return; const adminId = editId ? editId.value : ''; if (!adminId) return; if (btnSaveEdit) { btnSaveEdit.disabled = true; btnSaveEdit.textContent = '⏳ Guardando...'; } if (feedbackEdit) feedbackEdit.style.display = 'none'; try { const payload = { nombre: editName ? editName.value.trim() : '', documentoIdentidad: editDoc ? editDoc.value.trim() : '', email: editEmail ? editEmail.value.trim() : '', rol: editRole ? editRole.value : 'ADMIN' }; const res = await fetch(`/api/admin/administrators/${adminId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok && data.success) { if (feedbackEdit) { feedbackEdit.innerHTML = `
✅ Cambios guardados correctamente.
`; feedbackEdit.style.display = 'block'; } fetchAdministrators(); setTimeout(() => { closeEditModal(); }, 1200); } else { if (feedbackEdit) { feedbackEdit.innerHTML = `
❌ ${data.error || 'Error al guardar los cambios'}
`; feedbackEdit.style.display = 'block'; } } } catch (err) { console.error('Error updating admin:', err); } finally { if (btnSaveEdit) { btnSaveEdit.disabled = false; btnSaveEdit.textContent = '💾 Guardar Cambios'; } } }); } } async function fetchAdministrators() { const tbody = document.getElementById('table-administrators-body'); const counterLabel = document.getElementById('admins-counter-label'); const kpiAdmins = document.getElementById('kpi-admins-count'); const kpiOperators = document.getElementById('kpi-operators-count'); try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/admin/administrators', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (!res.ok || !data.success) { if (tbody) { tbody.innerHTML = ` ❌ ${data.error || 'Error al consultar equipo de administradores'} `; } return; } const admins = data.administrators || []; // Actualizar contadores let adminCount = 0; let operatorCount = 0; admins.forEach(a => { if (a.rol === 'OPERADOR') operatorCount++; else adminCount++; }); if (counterLabel) counterLabel.textContent = `${admins.length} miembro(s) registrados`; if (kpiAdmins) kpiAdmins.textContent = adminCount; if (kpiOperators) kpiOperators.textContent = operatorCount; if (!tbody) return; if (admins.length === 0) { tbody.innerHTML = ` No hay administradores registrados en la plataforma. `; return; } tbody.innerHTML = admins.map(admin => { const isSuper = !!admin.esSuperAdmin; const adminDataEncoded = encodeURIComponent(JSON.stringify(admin)); let roleBadge = ''; if (isSuper) { roleBadge = `👑 SUPERADMIN`; } else if (admin.rol === 'ADMIN') { roleBadge = `🛡️ ADMIN`; } else { roleBadge = `🎧 OPERADOR`; } let statusHtml = ''; if (isSuper) { statusHtml = ` 🔒 Permanente `; } else { const isActive = admin.isActive !== false; statusHtml = ` `; } let actionsHtml = ''; if (isSuper) { actionsHtml = ``; } else { actionsHtml = `
`; } return `
${isSuper ? '👑' : '🛡️'} ${admin.nombre} ${isSuper ? '👑 PRINCIPAL' : ''}
${admin.documentoIdentidad ? `C.C. ${admin.documentoIdentidad} • ` : ''}${admin.email || 'Sin correo registrado'}
📲 +${admin.telefono}
2FA OTP Habilitado
${roleBadge} ${statusHtml} ${actionsHtml} `; }).join(''); } catch (err) { console.error('Error fetching administrators:', err); if (tbody) { tbody.innerHTML = ` ❌ Error de conexión al cargar equipo de administradores `; } } } window.fetchAdministrators = fetchAdministrators; window.openEditAdminModal = function (adminDataEncoded) { try { const admin = JSON.parse(decodeURIComponent(adminDataEncoded)); const modal = document.getElementById('modal-edit-admin'); const editId = document.getElementById('edit-admin-id'); const editName = document.getElementById('edit-admin-name'); const editPhone = document.getElementById('edit-admin-phone'); const editDoc = document.getElementById('edit-admin-doc'); const editEmail = document.getElementById('edit-admin-email'); const editRole = document.getElementById('edit-admin-role'); const editRoleGroup = document.getElementById('edit-admin-role-group'); const subtitle = document.getElementById('edit-admin-subtitle'); const feedback = document.getElementById('edit-admin-feedback'); if (editId) editId.value = admin.id || ''; if (editName) editName.value = admin.nombre || ''; if (editPhone) editPhone.value = `+${admin.telefono || ''}`; if (editDoc) editDoc.value = admin.documentoIdentidad || ''; if (editEmail) editEmail.value = admin.email || ''; if (editRole) editRole.value = admin.rol || 'ADMIN'; if (admin.esSuperAdmin) { if (editRoleGroup) editRoleGroup.style.display = 'none'; if (subtitle) subtitle.textContent = 'Actualizar datos de contacto del Superadministrador permanente'; } else { if (editRoleGroup) editRoleGroup.style.display = 'block'; if (subtitle) subtitle.textContent = 'Actualizar permisos y datos de contacto del colaborador'; } if (feedback) feedback.style.display = 'none'; if (modal) modal.classList.add('active'); } catch (e) { console.error('Error parsing admin for edit:', e); } }; window.toggleAdminActive = async function (adminId, nextState, adminName) { try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch(`/api/admin/administrators/${adminId}/toggle-active`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ id: adminId, isActive: nextState }) }); const data = await res.json(); if (res.ok && data.success) { fetchAdministrators(); } else { alert(`Error al cambiar estado: ${data.error || 'No permitido'}`); } } catch (err) { console.error('Error toggling admin active:', err); alert('Error al comunicar con el servidor.'); } }; window.deleteAdmin = async function (adminId, adminName) { if (!confirm(`¿Estás seguro de eliminar a ${adminName || 'este administrador'} del equipo? Esta acción revoca inmediatamente sus permisos de acceso.`)) { return; } try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch(`/api/admin/administrators/${adminId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ id: adminId }) }); const data = await res.json(); if (res.ok && data.success) { fetchAdministrators(); } else { alert(`Error al eliminar administrador: ${data.error || 'No permitido'}`); } } catch (err) { console.error('Error deleting administrator:', err); alert('Error al comunicar con el servidor.'); } }; // ============================================================================== // GESTIÓN DE CIERRES MENSUALES & FACTURACIÓN DIAN (PACIFIC TRANS GO) // ============================================================================== let currentBillingCierres = []; let selectedBillingIds = new Set(); window.fetchMonthlyBillingReport = async function (customMonth, customYear) { const monthSelect = document.getElementById('select-billing-month'); const yearSelect = document.getElementById('select-billing-year'); const tbody = document.getElementById('table-billing-cierres-body'); const btnMark = document.getElementById('btn-billing-mark-invoiced'); const chkAll = document.getElementById('chk-billing-select-all'); const mes = customMonth || (monthSelect ? monthSelect.value : (new Date().getMonth() + 1)); const anio = customYear || (yearSelect ? yearSelect.value : new Date().getFullYear()); if (monthSelect) monthSelect.value = mes; if (yearSelect) yearSelect.value = anio; if (tbody) { tbody.innerHTML = `Cargando cierres fiscales del período ${mes}/${anio}...`; } selectedBillingIds.clear(); if (chkAll) chkAll.checked = false; if (btnMark) { btnMark.disabled = true; btnMark.textContent = '✅ Marcar Seleccionados como Facturado Manual'; } try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch(`/api/admin/billing/reporte-mensual?mes=${mes}&anio=${anio}`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (!data.success) { if (tbody) tbody.innerHTML = `Error al cargar reporte: ${data.error || 'Desconocido'}`; return; } currentBillingCierres = data.cierres || []; renderMonthlyBillingTable(currentBillingCierres, mes, anio); } catch (err) { console.error('Error fetching monthly billing report:', err); if (tbody) tbody.innerHTML = `Error de conexión con el servidor.`; } }; function renderMonthlyBillingTable(cierres, mes, anio) { const tbody = document.getElementById('table-billing-cierres-body'); const badge = document.getElementById('billing-results-badge'); const kpiConductores = document.getElementById('kpi-billing-conductores'); const kpiComisiones = document.getElementById('kpi-billing-total-comisiones'); const kpiViajes = document.getElementById('kpi-billing-total-viajes'); const kpiPendientes = document.getElementById('kpi-billing-pendientes'); const kpiFacturadosLabel = document.getElementById('kpi-billing-facturados-label'); if (badge) badge.textContent = `${cierres.length} registros`; let totalComisiones = 0; let totalViajes = 0; let pendientesCount = 0; let facturadosCount = 0; cierres.forEach(c => { totalComisiones += Number(c.total_comisiones_acumuladas || 0); totalViajes += Number(c.total_viajes_realizados || 0); if (c.estado_facturacion === 'facturado_manual') { facturadosCount++; } else { pendientesCount++; } }); if (kpiConductores) kpiConductores.textContent = cierres.length; if (kpiComisiones) kpiComisiones.textContent = `$${totalComisiones.toLocaleString('es-CO')}`; if (kpiViajes) kpiViajes.textContent = totalViajes; if (kpiPendientes) kpiPendientes.textContent = pendientesCount; if (kpiFacturadosLabel) kpiFacturadosLabel.textContent = `${facturadosCount} facturados manual (DIAN)`; if (!tbody) return; if (cierres.length === 0) { tbody.innerHTML = ` No se encontraron cierres para ${mes}/${anio}. Haz clic en ⚡ Ejecutar Corte Mensual Ahora para consolidar las comisiones de este mes. `; return; } tbody.innerHTML = cierres.map(c => { const id = c.id || c._id; const isFacturado = c.estado_facturacion === 'facturado_manual'; const isApi = c.estado_facturacion === 'enviado_api'; let estadoBadge = `⏳ Pendiente Proceso`; if (isFacturado) { estadoBadge = `✅ Facturado Manual (DIAN)`; } else if (isApi) { estadoBadge = `🌐 Enviado API`; } const fechaCierreFmt = c.fecha_cierre ? new Date(c.fecha_cierre).toLocaleDateString('es-CO') : '-'; const checked = selectedBillingIds.has(id) ? 'checked' : ''; return ` ${id.slice(0, 8)}... ${c.nit_o_cedula || 'NO-REGISTRADO'} ${c.razon_social || 'Conductor Pacific Trans'} ${c.correo_electronico || 'info@pacifictrans.com'} $${Number(c.total_comisiones_acumuladas || 0).toLocaleString('es-CO')} ${c.total_viajes_realizados || 0} ${estadoBadge} ${fechaCierreFmt} `; }).join(''); attachBillingItemCheckboxListeners(); } function attachBillingItemCheckboxListeners() { const itemCheckboxes = document.querySelectorAll('.chk-billing-item'); const btnMark = document.getElementById('btn-billing-mark-invoiced'); const chkAll = document.getElementById('chk-billing-select-all'); itemCheckboxes.forEach(chk => { chk.addEventListener('change', () => { if (chk.checked) { selectedBillingIds.add(chk.value); } else { selectedBillingIds.delete(chk.value); } if (btnMark) { btnMark.disabled = selectedBillingIds.size === 0; btnMark.textContent = selectedBillingIds.size > 0 ? `✅ Marcar (${selectedBillingIds.size}) como Facturado Manual` : '✅ Marcar Seleccionados como Facturado Manual'; } if (chkAll) { chkAll.checked = itemCheckboxes.length > 0 && selectedBillingIds.size === itemCheckboxes.length; } }); }); } // Inicialización de Eventos de Facturación DIAN document.addEventListener('DOMContentLoaded', () => { const btnFilter = document.getElementById('btn-billing-filter'); const btnDownloadCsv = document.getElementById('btn-billing-download-csv'); const btnRunCut = document.getElementById('btn-billing-run-cut'); const chkAll = document.getElementById('chk-billing-select-all'); const btnMarkInvoiced = document.getElementById('btn-billing-mark-invoiced'); const monthSelect = document.getElementById('select-billing-month'); const yearSelect = document.getElementById('select-billing-year'); if (btnFilter) { btnFilter.addEventListener('click', () => { window.fetchMonthlyBillingReport(); }); } if (monthSelect) { monthSelect.addEventListener('change', () => window.fetchMonthlyBillingReport()); } if (yearSelect) { yearSelect.addEventListener('change', () => window.fetchMonthlyBillingReport()); } // Descarga directa del archivo CSV con columnas requeridas por DIAN if (btnDownloadCsv) { btnDownloadCsv.addEventListener('click', () => { const mes = monthSelect ? monthSelect.value : (new Date().getMonth() + 1); const anio = yearSelect ? yearSelect.value : new Date().getFullYear(); const downloadUrl = `/api/admin/billing/reporte-mensual?mes=${mes}&anio=${anio}&format=csv`; window.open(downloadUrl, '_blank'); }); } // Ejecución del corte mensual bajo demanda if (btnRunCut) { btnRunCut.addEventListener('click', async () => { const mes = monthSelect ? monthSelect.value : (new Date().getMonth() + 1); const anio = yearSelect ? yearSelect.value : new Date().getFullYear(); if (!confirm(`¿Deseas consolidar y generar el corte mensual de comisiones para ${mes}/${anio}?`)) { return; } btnRunCut.disabled = true; btnRunCut.textContent = '⏳ Ejecutando corte...'; try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/admin/billing/ejecutar-cierre', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ mes: parseInt(mes, 10), anio: parseInt(anio, 10) }) }); const data = await res.json(); if (res.ok && data.success) { alert(`✅ Corte mensual completado exitosamente para ${mes}/${anio}.\nTotal Conductores: ${data.totalConductores}\nTotal Comisiones de Software: $${Number(data.totalComisiones).toLocaleString('es-CO')} COP`); window.fetchMonthlyBillingReport(mes, anio); } else { alert(`Error al ejecutar corte: ${data.error || 'Fallo interno'}`); } } catch (err) { console.error('Error executing billing cut:', err); alert('Error de comunicación con el servidor.'); } finally { btnRunCut.disabled = false; btnRunCut.textContent = '⚡ Ejecutar Corte Mensual Ahora'; } }); } // Checkbox Seleccionar Todos if (chkAll) { chkAll.addEventListener('change', () => { const itemCheckboxes = document.querySelectorAll('.chk-billing-item'); itemCheckboxes.forEach(chk => { chk.checked = chkAll.checked; if (chkAll.checked) { selectedBillingIds.add(chk.value); } else { selectedBillingIds.delete(chk.value); } }); if (btnMarkInvoiced) { btnMarkInvoiced.disabled = selectedBillingIds.size === 0; btnMarkInvoiced.textContent = selectedBillingIds.size > 0 ? `✅ Marcar (${selectedBillingIds.size}) como Facturado Manual` : '✅ Marcar Seleccionados como Facturado Manual'; } }); } // Marcar seleccionados como facturados manualmente if (btnMarkInvoiced) { btnMarkInvoiced.addEventListener('click', async () => { if (selectedBillingIds.size === 0) return; const count = selectedBillingIds.size; if (!confirm(`¿Confirmas marcar ${count} cierres seleccionados como 'Facturado Manual' en la DIAN? Esto evitará duplicidades fiscales.`)) { return; } btnMarkInvoiced.disabled = true; btnMarkInvoiced.textContent = '⏳ Actualizando...'; try { const token = localStorage.getItem('pt_token') || ''; const res = await fetch('/api/admin/billing/marcar-facturado', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ IDs_Cierre: Array.from(selectedBillingIds) }) }); const data = await res.json(); if (res.ok && data.success) { alert(`✅ ${data.updatedCount} cierres actualizados a estado 'facturado_manual'.`); selectedBillingIds.clear(); window.fetchMonthlyBillingReport(); } else { alert(`Error al actualizar estado: ${data.error || 'No permitido'}`); } } catch (err) { console.error('Error marking closures as invoiced:', err); alert('Error de conexión con el servidor.'); } finally { if (btnMarkInvoiced) { btnMarkInvoiced.disabled = selectedBillingIds.size === 0; btnMarkInvoiced.textContent = '✅ Marcar Seleccionados como Facturado Manual'; } } }); } });