// Pacifictrans Go - Interactive Web Platform Logic let currentTariffs = { tarifaPasajeroFija: 35000, precioBasePaquete: 25000, precioKmPaquete: 1200, factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 }, seguroBasePaquete: 5000, porcentajeComisionAdmin: 15, costoPenalidadCancelacion: 15000, radioMaxPoolingMetros: 4000, numeroAtencionWhatsApp: '573167455877', nombreSoporte: 'Atención Pacifictrans Go' }; let currentUser = null; let leafletMap = null; let deferredPrompt = null; // Drivers Flota con coordenadas reales en el corredor Cali - Buenaventura const activeDrivers = [ { id: '1', nombre: 'Mario Fernando G.', vehiculo: 'Renault Duster (WLZ-890)', barrio: 'Granada (Comuna 2)', distCali: '1.2 km', lat: 3.4552, lng: -76.5350, estado: 'DISPONIBLE' }, { id: '2', nombre: 'Hernán Darío P.', vehiculo: 'Toyota Fortuner (VBF-312)', barrio: 'San Antonio (Comuna 3)', distCali: '2.4 km', lat: 3.4485, lng: -76.5415, estado: 'ASIGNADO' }, { id: '3', nombre: 'Mauricio V.', vehiculo: 'Chevrolet Tracker (HJK-456)', barrio: 'El Ingenio (Comuna 17)', distCali: '6.8 km', lat: 3.3850, lng: -76.5290, estado: 'DISPONIBLE' }, { id: '4', nombre: 'Javier Restrepo', vehiculo: 'Nissan Kicks (RTY-789)', barrio: 'Limonar (Comuna 19)', distCali: '5.1 km', lat: 3.6580, lng: -76.6900, estado: 'EN_RUTA' } ]; document.addEventListener('DOMContentLoaded', () => { initPWA(); initTabs(); initAuthSystem(); initNotificationsSystem(); initPassengerCalculator(); initFreightCalculator(); initLeafletRadarMap(); initReviewsAndStats(); initTariffsManager(); initSettlementsAndBreBSystem(); fetchTariffs(); fetchStatsAndReviews(); }); /* ========================================================================== 1. PWA & IPHONE INSTALLATION SYSTEM ========================================================================== */ function initPWA() { if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('✅ [PWA] Service Worker registrado exitosamente:', reg.scope)) .catch(err => console.warn('⚠️ [PWA Service Worker Warning]:', err)); }); } // Guardar evento de instalación de Android window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; }); const btnInstall = document.getElementById('btn-install-pwa'); const modalInstall = document.getElementById('modal-pwa-install'); const btnClose = document.getElementById('btn-close-pwa-modal'); const btnTriggerAndroid = document.getElementById('btn-trigger-android-pwa'); if (btnInstall && modalInstall) { btnInstall.addEventListener('click', () => { modalInstall.classList.add('active'); }); } if (btnClose && modalInstall) { btnClose.addEventListener('click', () => { modalInstall.classList.remove('active'); }); modalInstall.addEventListener('click', (e) => { if (e.target === modalInstall) modalInstall.classList.remove('active'); }); } if (btnTriggerAndroid) { btnTriggerAndroid.addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log('PWA Outcome:', outcome); deferredPrompt = null; if (modalInstall) modalInstall.classList.remove('active'); } else { alert('Para instalar en iPhone, sigue los 4 pasos mostrados arriba en Safari. En Android Chrome, toca los 3 puntos (⋮) y selecciona "Instalar aplicación".'); } }); } } /* ========================================================================== 2. TABS & ADMIN SUBTABS NAVIGATION ========================================================================== */ function initTabs() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); 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'); } }); // Si se activa el radar, refrescar el tamaño del mapa Leaflet if (targetId === 'tab-admin' && leafletMap) { setTimeout(() => { leafletMap.invalidateSize(); }, 250); } }; tabBtns.forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-tab'); window.activateTab(targetId); }); }); // Enlaces de la barra de navegación const navBtnAdmin = document.getElementById('nav-btn-admin'); if (navBtnAdmin) { navBtnAdmin.addEventListener('click', (e) => { e.preventDefault(); window.activateTab('tab-admin'); document.getElementById('admin')?.scrollIntoView({ behavior: 'smooth' }); }); } const navBtnStats = document.getElementById('nav-btn-stats'); if (navBtnStats) { navBtnStats.addEventListener('click', (e) => { e.preventDefault(); window.activateTab('tab-estadisticas'); document.getElementById('tab-estadisticas')?.scrollIntoView({ behavior: 'smooth' }); }); } // Sub-tabs de la sección administrativa const subRadarBtn = document.getElementById('btn-admin-subtab-radar'); const subTarifasBtn = document.getElementById('btn-admin-subtab-tarifas'); const subLiquidacionesBtn = document.getElementById('btn-admin-subtab-liquidaciones'); const viewRadar = document.getElementById('admin-view-radar'); const viewTarifas = document.getElementById('admin-view-tarifas'); const viewLiquidaciones = document.getElementById('admin-view-liquidaciones'); function setAdminSubTab(activeBtn, activeView) { [subRadarBtn, subTarifasBtn, subLiquidacionesBtn].forEach(b => b?.classList.remove('active')); [viewRadar, viewTarifas, viewLiquidaciones].forEach(v => { if (v) v.style.display = 'none'; }); if (activeBtn) activeBtn.classList.add('active'); if (activeView) activeView.style.display = 'block'; } if (subRadarBtn && viewRadar) { subRadarBtn.addEventListener('click', () => { setAdminSubTab(subRadarBtn, viewRadar); if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 200); }); } if (subTarifasBtn && viewTarifas) { subTarifasBtn.addEventListener('click', () => { setAdminSubTab(subTarifasBtn, viewTarifas); }); } if (subLiquidacionesBtn && viewLiquidaciones) { subLiquidacionesBtn.addEventListener('click', () => { setAdminSubTab(subLiquidacionesBtn, viewLiquidaciones); if (window.fetchSettlementsOverview) window.fetchSettlementsOverview(); }); } // Sub-tabs de la sección del conductor const subDriverWalletBtn = document.getElementById('btn-driver-subtab-billetera'); const subDriverRegistroBtn = document.getElementById('btn-driver-subtab-registro'); const viewDriverWallet = document.getElementById('driver-view-billetera'); const viewDriverRegistro = document.getElementById('driver-view-registro'); if (subDriverWalletBtn && subDriverRegistroBtn && viewDriverWallet && viewDriverRegistro) { subDriverWalletBtn.addEventListener('click', () => { subDriverWalletBtn.classList.add('active'); subDriverRegistroBtn.classList.remove('active'); viewDriverWallet.style.display = 'block'; viewDriverRegistro.style.display = 'none'; if (window.fetchDriverWallet) window.fetchDriverWallet(); }); subDriverRegistroBtn.addEventListener('click', () => { subDriverRegistroBtn.classList.add('active'); subDriverWalletBtn.classList.remove('active'); viewDriverWallet.style.display = 'none'; viewDriverRegistro.style.display = 'block'; }); } } /* ========================================================================== 3. AUTHENTICATION (DRIPZONE STYLE CON OTP WHATSAPP + EMAIL) ========================================================================== */ function initAuthSystem() { const modal = document.getElementById('modal-auth-dripzone'); const btnOpen = document.getElementById('btn-open-auth-modal'); const btnClose = document.getElementById('btn-close-auth-modal'); const btnLogout = document.getElementById('btn-logout'); const modeLoginBtn = document.getElementById('btn-auth-mode-login'); const modeRegisterBtn = document.getElementById('btn-auth-mode-register'); const groupName = document.getElementById('group-auth-name'); const authModalTitle = document.getElementById('auth-modal-title'); const step1 = document.getElementById('auth-step-1'); const step2 = document.getElementById('auth-step-2'); const formStep1 = document.getElementById('form-auth-request-otp'); const btnVerifyOtp = document.getElementById('btn-submit-verify-otp'); const btnResend = document.getElementById('btn-auth-resend-otp'); const reqError = document.getElementById('auth-request-error'); const verError = document.getElementById('auth-verify-error'); const targetLabel = document.getElementById('auth-sent-target'); let authMode = 'login'; // 'login' | 'register' let currentIdentifier = ''; let currentEmail = ''; // Abrir y cerrar modal if (btnOpen && modal) { btnOpen.addEventListener('click', () => { reqError.style.display = 'none'; verError.style.display = 'none'; step1.style.display = 'block'; step2.style.display = 'none'; modal.classList.add('active'); }); } if (btnClose && modal) { btnClose.addEventListener('click', () => modal.classList.remove('active')); modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('active'); }); } // Alternar entre Login y Registro if (modeLoginBtn && modeRegisterBtn) { modeLoginBtn.addEventListener('click', () => { authMode = 'login'; modeLoginBtn.classList.add('active'); modeRegisterBtn.classList.remove('active'); if (groupName) groupName.style.display = 'none'; if (authModalTitle) authModalTitle.textContent = 'Acceso Seguro con Verificación OTP'; }); modeRegisterBtn.addEventListener('click', () => { authMode = 'register'; modeRegisterBtn.classList.add('active'); modeLoginBtn.classList.remove('active'); if (groupName) groupName.style.display = 'block'; if (authModalTitle) authModalTitle.textContent = 'Registro de Usuario & Validación 2FA'; }); } // Paso 1: Enviar OTP por WhatsApp if (formStep1) { formStep1.addEventListener('submit', async (e) => { e.preventDefault(); const phoneInput = document.getElementById('auth-input-phone'); const emailInput = document.getElementById('auth-input-email'); const nameInput = document.getElementById('auth-input-name'); const btnSubmit = document.getElementById('btn-submit-send-otp'); const phone = phoneInput?.value.trim() || ''; const email = emailInput?.value.trim() || ''; const name = nameInput?.value.trim() || ''; if (!phone) { showError(reqError, 'Ingresa tu número de WhatsApp.'); return; } currentIdentifier = phone; currentEmail = email; btnSubmit.disabled = true; btnSubmit.innerHTML = '⏳ Enviando código de seguridad...'; reqError.style.display = 'none'; try { const res = await fetch('/api/auth/send-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: phone, email }) }); const data = await res.json(); if (res.ok && data.success) { step1.style.display = 'none'; step2.style.display = 'block'; if (targetLabel) targetLabel.textContent = data.maskedPhone || `+${phone}`; // Si el servidor devolvió preview (entorno local sin SIM de Evolution), mostrar aviso if (data.codePreview) { console.log(`[Demo Code OTP]: ${data.codePreview}`); verError.style.display = 'block'; verError.style.color = '#38bdf8'; verError.innerHTML = `ℹ️ Código de prueba generado: ${data.codePreview}`; } focusFirstOtp(); } else { showError(reqError, data.error || 'No se pudo enviar el código OTP.'); } } catch (err) { showError(reqError, 'Error de conexión. Inténtalo de nuevo.'); } finally { btnSubmit.disabled = false; btnSubmit.innerHTML = '📲 Enviar Código de Verificación OTP'; } }); } // Cajas OTP con avance automático y soporte de pegado const otpInputs = [ document.getElementById('otp-1'), document.getElementById('otp-2'), document.getElementById('otp-3'), document.getElementById('otp-4'), document.getElementById('otp-5'), document.getElementById('otp-6') ]; otpInputs.forEach((input, index) => { if (!input) return; input.addEventListener('input', (e) => { const val = e.target.value; if (val.length === 1 && index < 5) { otpInputs[index + 1].focus(); } checkAutoSubmitOtp(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !input.value && index > 0) { otpInputs[index - 1].focus(); } }); input.addEventListener('paste', (e) => { e.preventDefault(); const pasteData = e.clipboardData.getData('text').trim(); if (/^\d{6}$/.test(pasteData)) { pasteData.split('').forEach((digit, i) => { if (otpInputs[i]) otpInputs[i].value = digit; }); checkAutoSubmitOtp(); } }); }); function focusFirstOtp() { otpInputs.forEach(i => { if (i) i.value = ''; }); if (otpInputs[0]) otpInputs[0].focus(); } function getEnteredOtp() { return otpInputs.map(i => i?.value || '').join(''); } function checkAutoSubmitOtp() { const code = getEnteredOtp(); if (code.length === 6) { submitVerifyOtp(code); } } // Paso 2: Verificar OTP if (btnVerifyOtp) { btnVerifyOtp.addEventListener('click', () => { const code = getEnteredOtp(); if (code.length < 6) { showError(verError, 'Ingresa los 6 dígitos del código.'); return; } submitVerifyOtp(code); }); } async function submitVerifyOtp(code) { if (!btnVerifyOtp) return; btnVerifyOtp.disabled = true; btnVerifyOtp.innerHTML = '⏳ Verificando código...'; verError.style.display = 'none'; const nameInput = document.getElementById('auth-input-name'); const name = nameInput?.value.trim() || ''; try { const res = await fetch('/api/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: currentIdentifier, code, nombre: name, email: currentEmail }) }); const data = await res.json(); if (res.ok && data.success) { localStorage.setItem('pt_token', data.token); currentUser = data.user; updateUserUI(currentUser); modal.classList.remove('active'); alert(`¡Bienvenido a Pacifictrans Go, ${currentUser.nombre}! Tu identidad ha sido verificada con éxito.`); } else { showError(verError, data.error || 'Código incorrecto. Verifica los 6 dígitos.'); } } catch (err) { showError(verError, 'Error al validar el código OTP.'); } finally { btnVerifyOtp.disabled = false; btnVerifyOtp.innerHTML = '✅ Validar y Acceder'; } } // Reenviar OTP if (btnResend) { btnResend.addEventListener('click', () => { formStep1.dispatchEvent(new Event('submit')); }); } // Cerrar sesión if (btnLogout) { btnLogout.addEventListener('click', () => { localStorage.removeItem('pt_token'); currentUser = null; updateUserUI(null); }); } // Validar sesión persistida checkPersistedSession(); } async function checkPersistedSession() { const token = localStorage.getItem('pt_token'); if (!token) return; try { const res = await fetch(`/api/auth/me?token=${token}`); if (res.ok) { const data = await res.json(); if (data.user) { currentUser = data.user; updateUserUI(currentUser); } } else { localStorage.removeItem('pt_token'); } } catch (err) { console.warn('⚠️ [Session Verify Error]:', err); } } function updateUserUI(user) { const btnAuthOpen = document.getElementById('btn-open-auth-modal'); const userBadge = document.getElementById('user-profile-badge'); const userNameEl = document.getElementById('user-display-name'); const inputUserName = document.getElementById('user-name'); const inputUserPhone = document.getElementById('user-phone'); const inputSenderPhone = document.getElementById('sender-phone'); if (user) { if (btnAuthOpen) btnAuthOpen.style.display = 'none'; if (userBadge) userBadge.style.display = 'inline-flex'; if (userNameEl) userNameEl.textContent = user.nombre.split(' ')[0]; // Autofill en formularios de la plataforma if (inputUserName && !inputUserName.value) inputUserName.value = user.nombre; if (inputUserPhone && !inputUserPhone.value) inputUserPhone.value = user.telefono; if (inputSenderPhone && !inputSenderPhone.value) inputSenderPhone.value = user.telefono; } else { if (btnAuthOpen) btnAuthOpen.style.display = 'inline-flex'; if (userBadge) userBadge.style.display = 'none'; } } function showError(el, msg) { if (!el) return; el.textContent = msg; el.style.display = 'block'; } /* ========================================================================== 4. LEAFLET RADAR FLEET MAP (MAPA ESTÁNDAR CON TOOLTIPS) ========================================================================== */ function initLeafletRadarMap() { const mapContainer = document.getElementById('fleet-live-map'); if (!mapContainer || typeof L === 'undefined') return; // Centro entre Cali (3.45) y Buenaventura (3.88) const corridorCenter = [3.65, -76.78]; leafletMap = L.map('fleet-live-map', { center: corridorCenter, zoom: 10, zoomControl: true, scrollWheelZoom: false }); // Capa de Mapa Estándar OpenStreetMap L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '© OpenStreetMap contributors • Pacifictrans Go GPS' }).addTo(leafletMap); // Línea poligonal oficial de la Carretera Vía al Mar (Ruta 40) const corridorWaypoints = [ [3.4516, -76.5320], // Cali [3.4750, -76.5650], // Portada al Mar [3.5800, -76.6300], // Km 18 / Saladito [3.6580, -76.6900], // Dagua [3.7650, -76.7350], // Loboguerrero [3.7920, -76.8150], // Cisneros [3.8450, -76.9200], // Córdoba [3.8814, -77.0315] // Buenaventura ]; L.polyline(corridorWaypoints, { color: '#06b6d4', weight: 5, opacity: 0.85, dashArray: '8, 6' }).addTo(leafletMap); // Marcadores de Hitos Urbanos addCityMarker(leafletMap, [3.4516, -76.5320], '🟢 Cali (Km 0)'); addCityMarker(leafletMap, [3.6580, -76.6900], '🔵 Dagua (Km 45)'); addCityMarker(leafletMap, [3.7650, -76.7350], '🔵 Loboguerrero (Km 60)'); addCityMarker(leafletMap, [3.8814, -77.0315], '🟡 Buenaventura (Km 115)'); // Vehículos de la Flota en Tiempo Real activeDrivers.forEach((driver, index) => { const customIcon = L.divIcon({ className: 'vehicle-marker-icon', html: `
🚗
`, iconSize: [44, 44], iconAnchor: [22, 22] }); const marker = L.marker([driver.lat, driver.lng], { icon: customIcon }).addTo(leafletMap); // Tooltip enriquecido al pasar el cursor o pulsar const tooltipContent = `
#${index + 1} ${driver.nombre} ● ${driver.estado}
${driver.vehiculo}
🏠 Domicilio: ${driver.barrio}
📍 Cercanía a 1er Pasajero: ${driver.distCali}
`; marker.bindTooltip(tooltipContent, { direction: 'top', className: 'custom-vehicle-tooltip', offset: [0, -20] }); marker.on('click', () => { window.asignarConductorDemo(driver.nombre, driver.barrio); }); }); renderSuggestedDriversList(); } function addCityMarker(map, coords, label) { const icon = L.divIcon({ className: 'city-marker-icon', html: `
${label}
`, iconSize: [110, 26], iconAnchor: [55, 13] }); L.marker(coords, { icon }).addTo(map); } function renderSuggestedDriversList() { const driverListEl = document.getElementById('suggested-drivers-list'); if (!driverListEl) return; driverListEl.innerHTML = activeDrivers.map((d, index) => `
#${index + 1} ${d.nombre}
${d.vehiculo}
🏠 Residencia en Cali: ${d.barrio}
Distancia al 1er Pasajero: ${d.distCali}
${d.estado}
`).join(''); } window.asignarConductorDemo = function(conductor, barrio) { const resultDiv = document.getElementById('schedule-dispatch-result'); if (resultDiv) { resultDiv.style.display = 'block'; resultDiv.innerHTML = `

✅ Conductor Asignado por Residencia: ${conductor} (${barrio})

🚀 Disparo Inmediato de Notificación WhatsApp Ejecutado: Los 4 pasajeros han recibido su confirmación oficial con el conductor asignado, vehículo, hora estimada escalonada de recogida y enlace de Waze en vivo.

📲 [Evolution API WhatsApp Status: 201 Created] Enviado a los 4 Pasajeros + Conductor.
`; resultDiv.scrollIntoView({ behavior: 'smooth' }); } }; /* ========================================================================== 5. TRIP STATISTICS & COMMUNITY REVIEWS ========================================================================== */ function initReviewsAndStats() { const btnOpenModal = document.getElementById('btn-open-review-modal'); const modalReview = document.getElementById('modal-add-review'); const btnCloseModal = document.getElementById('btn-close-review-modal'); const formReview = document.getElementById('form-submit-review'); const starPicker = document.getElementById('star-picker'); const hiddenScore = document.getElementById('review-input-score'); if (btnOpenModal && modalReview) { btnOpenModal.addEventListener('click', () => { if (currentUser && document.getElementById('review-input-name')) { document.getElementById('review-input-name').value = currentUser.nombre; } modalReview.classList.add('active'); }); } if (btnCloseModal && modalReview) { btnCloseModal.addEventListener('click', () => modalReview.classList.remove('active')); modalReview.addEventListener('click', (e) => { if (e.target === modalReview) modalReview.classList.remove('active'); }); } // Selector interactivo de estrellas (1-5) if (starPicker) { const stars = starPicker.querySelectorAll('span'); stars.forEach(star => { star.addEventListener('click', () => { const val = parseInt(star.getAttribute('data-star') || '5', 10); if (hiddenScore) hiddenScore.value = val; stars.forEach((s, idx) => { if (idx < val) s.classList.add('selected'); else s.classList.remove('selected'); }); }); }); } // Envío de reseña if (formReview) { formReview.addEventListener('submit', async (e) => { e.preventDefault(); const btnSubmit = document.getElementById('btn-submit-review'); const score = parseFloat(hiddenScore?.value || '5'); const name = document.getElementById('review-input-name')?.value.trim(); const role = document.getElementById('review-input-role')?.value; const target = document.getElementById('review-input-target')?.value.trim(); const comment = document.getElementById('review-input-comment')?.value.trim(); if (!name || !comment) return; btnSubmit.disabled = true; btnSubmit.innerHTML = '⏳ Publicando reseña...'; try { const res = await fetch('/api/stats-reviews', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autorId: currentUser?.id || `anon-${Date.now()}`, autorNombre: name, autorRol: role, destinatarioNombre: target, puntuacion: score, comentario: comment, etiquetas: ['Puntualidad ⏱️', 'Servicio Seguro 🛡️', 'Verificado ✅'] }) }); if (res.ok) { formReview.reset(); if (modalReview) modalReview.classList.remove('active'); fetchStatsAndReviews(); alert('¡Gracias por tu calificación! Ha sido agregada al muro de la comunidad.'); } } catch (err) { console.warn('Error al enviar reseña:', err); } finally { btnSubmit.disabled = false; btnSubmit.innerHTML = '⭐ Publicar Mi Calificación'; } }); } } async function fetchStatsAndReviews() { try { const res = await fetch('/api/stats-reviews'); if (res.ok) { const data = await res.json(); if (data.stats?.global) { const g = data.stats.global; const totalViajesEl = document.getElementById('stat-total-viajes'); const ratingAvgEl = document.getElementById('stat-rating-avg'); const puntualidadEl = document.getElementById('stat-puntualidad'); const conductoresActivosEl = document.getElementById('stat-conductores-activos'); if (totalViajesEl) totalViajesEl.textContent = `${g.totalViajes}+`; if (ratingAvgEl) ratingAvgEl.textContent = `${g.calificacionPromedio} / 5.0`; if (puntualidadEl) puntualidadEl.textContent = g.indicePuntualidad; if (conductoresActivosEl) conductoresActivosEl.textContent = g.conductoresCertificados; } if (data.recentReviews && Array.isArray(data.recentReviews)) { renderReviewsGrid(data.recentReviews); } } } catch (err) { console.warn('⚠️ [Stats/Reviews Error]:', err); } } function renderReviewsGrid(reviews) { const container = document.getElementById('reviews-list-grid'); if (!container) return; container.innerHTML = reviews.map(r => { const starsCount = Math.round(r.puntuacion || 5); const starsString = '★'.repeat(starsCount) + '☆'.repeat(5 - starsCount); const tagsHtml = (r.etiquetas || []).map(t => `${t}`).join(''); const isPasajero = r.autorRol === 'PASAJERO'; return `
${r.autorNombre}
${isPasajero ? '👤 Pasajero Verificado' : '🚗 Conductor Certificado'}
${starsString}
Ruta: ${r.ruta || 'Cali ➔ Buenaventura'} • Calificó a: ${r.destinatarioNombre}
"${r.comentario}"
${tagsHtml}
`; }).join(''); } /* ========================================================================== 6. CONFIGURACIÓN DE TARIFAS Y CANAL DE ATENCIÓN WHATSAPP ========================================================================== */ async function fetchTariffs() { try { const res = await fetch('/api/tariffs'); if (res.ok) { const data = await res.json(); if (data.config) { currentTariffs = { ...currentTariffs, ...data.config }; syncTariffsToUI(); } } } catch (err) { console.warn('⚠️ [Tarifas API]: Modo local/offline activo, utilizando tarifas por defecto.', err); syncTariffsToUI(); } } function syncTariffsToUI() { // 1. Formulario administrativo de tarifas y soporte 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'); if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija; if (elBasePaquete) elBasePaquete.value = currentTariffs.precioBasePaquete; if (elKmPaquete) elKmPaquete.value = currentTariffs.precioKmPaquete; if (elSobre && currentTariffs.factorPaquetes) elSobre.value = currentTariffs.factorPaquetes.sobre || 1.0; if (elMediano && currentTariffs.factorPaquetes) elMediano.value = currentTariffs.factorPaquetes.mediano || 1.6; if (elGrande && currentTariffs.factorPaquetes) elGrande.value = currentTariffs.factorPaquetes.grande || 2.2; if (elSeguro) elSeguro.value = currentTariffs.seguroBasePaquete; if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin; if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion; if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros; if (elPhone && currentTariffs.numeroAtencionWhatsApp) elPhone.value = currentTariffs.numeroAtencionWhatsApp; if (elName && currentTariffs.nombreSoporte) elName.value = currentTariffs.nombreSoporte; // 2. Sincronización del enlace dinámico de atención WhatsApp en el footer y navbar const phoneFormatted = formatPhoneNumber(currentTariffs.numeroAtencionWhatsApp || '573167455877'); const cleanPhone = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, ''); const footerPhoneEl = document.getElementById('footer-support-phone'); const footerLinkEl = document.getElementById('footer-support-link'); if (footerPhoneEl) footerPhoneEl.textContent = phoneFormatted; if (footerLinkEl) footerLinkEl.href = `https://wa.me/${cleanPhone}?text=Hola%20Pacifictrans%20Go,%20deseo%20informaci%C3%B3n%20sobre%20un%20servicio`; // 3. Precios en módulo de reservas de pasajeros const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`; const totalPriceEl = document.querySelector('.total-price'); if (totalPriceEl) totalPriceEl.textContent = formattedPasajero; const btnBook = document.getElementById('btn-book-pool'); if (btnBook && !btnBook.disabled) { btnBook.innerHTML = ` Confirmar y Pagar Cupo (${formattedPasajero}) `; } // 4. Recalcular módulo de mensajería y fletes if (window.recalculateFreight) { window.recalculateFreight(); } } function formatPhoneNumber(num) { const clean = num.replace(/\D/g, ''); if (clean.startsWith('57') && clean.length === 12) { return `+57 ${clean.slice(2, 5)} ${clean.slice(5, 8)} ${clean.slice(8)}`; } return `+${clean}`; } function initTariffsManager() { const form = document.getElementById('form-tariffs-config'); const statusEl = document.getElementById('tariffs-save-status'); if (!form) return; form.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('btn-save-tariffs'); if (btn) { btn.disabled = true; btn.innerHTML = '⏳ Guardando en MongoDB...'; } const payload = { tarifaPasajeroFija: parseFloat(document.getElementById('cfg-tarifa-pasajero')?.value || '35000'), precioBasePaquete: parseFloat(document.getElementById('cfg-base-paquete')?.value || '25000'), precioKmPaquete: parseFloat(document.getElementById('cfg-km-paquete')?.value || '1200'), factorPaquetes: { sobre: parseFloat(document.getElementById('cfg-factor-sobre')?.value || '1.0'), mediano: parseFloat(document.getElementById('cfg-factor-mediano')?.value || '1.6'), grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2') }, seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'), porcentajeComisionAdmin: parseFloat(document.getElementById('cfg-comision')?.value || '15'), costoPenalidadCancelacion: parseFloat(document.getElementById('cfg-penalidad')?.value || '15000'), radioMaxPoolingMetros: parseFloat(document.getElementById('cfg-radio-pooling')?.value || '4000'), numeroAtencionWhatsApp: document.getElementById('cfg-admin-phone')?.value.trim() || '573167455877', nombreSoporte: document.getElementById('cfg-admin-name')?.value.trim() || 'Atención Pacifictrans Go' }; try { const res = await fetch('/api/tariffs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, 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 | WhatsApp Atención: +${payload.numeroAtencionWhatsApp}`; statusEl.style.color = '#10b981'; } } else { throw new Error('Error al guardar en el servidor'); } } catch (err) { currentTariffs = { ...currentTariffs, ...payload }; syncTariffsToUI(); if (statusEl) { statusEl.innerHTML = `✅ Configuración actualizada localmente.`; statusEl.style.color = '#10b981'; } } finally { if (btn) { btn.disabled = false; btn.innerHTML = '💾 Guardar y Aplicar Configuración'; } } }); } /* ========================================================================== 7. RESERVA DE PASAJEROS & POOLING ========================================================================== */ function initPassengerCalculator() { const originSelect = document.getElementById('pool-origin'); const addressGroup = document.getElementById('cali-address-group'); const buenaStopGroup = document.getElementById('buena-stop-group'); const bookBtn = document.getElementById('btn-book-pool'); const poolDots = document.querySelectorAll('.seat-dot'); let currentSeatsFilled = 3; function updateOriginDestination() { if (!originSelect) return; const isCali = originSelect.value === 'CALI'; if (addressGroup) addressGroup.style.display = isCali ? 'block' : 'none'; if (buenaStopGroup) buenaStopGroup.style.display = isCali ? 'block' : 'none'; } if (originSelect) { originSelect.addEventListener('change', updateOriginDestination); updateOriginDestination(); } if (bookBtn) { bookBtn.addEventListener('click', async (e) => { e.preventDefault(); // Verificación de seguridad: si no ha validado identidad con OTP, pedir autenticación if (!currentUser) { alert('🛡️ Por seguridad de los conductores y de los demás pasajeros, debes verificar tu identidad con OTP antes de confirmar tu reserva.'); document.getElementById('modal-auth-dripzone')?.classList.add('active'); return; } bookBtn.disabled = true; bookBtn.innerHTML = ` Procesando Reserva... `; setTimeout(() => { currentSeatsFilled = 4; updateSeatVisuals(currentSeatsFilled); const alertBox = document.getElementById('pool-dispatch-alert'); if (alertBox) { alertBox.style.display = 'block'; alertBox.scrollIntoView({ behavior: 'smooth' }); } bookBtn.disabled = false; const formatted = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`; bookBtn.innerHTML = `✅ Cupo Reservado Exitosamente (${formatted})`; }, 1200); }); } function updateSeatVisuals(count) { poolDots.forEach((dot, index) => { dot.classList.remove('active', 'pending'); if (index < count) { dot.classList.add('active'); } else if (index === count) { dot.classList.add('pending'); } }); const seatCounter = document.getElementById('seat-counter-text'); if (seatCounter) seatCounter.textContent = `${count}/4 Pasajeros`; } updateSeatVisuals(currentSeatsFilled); } /* ========================================================================== 8. CALCULADORA DE MENSAJERÍA & FLETES ========================================================================== */ function initFreightCalculator() { const packageTypeSelect = document.getElementById('package-type'); const declaredValueInput = document.getElementById('declared-value'); const totalPriceEl = document.getElementById('freight-total-price'); const baseCostEl = document.getElementById('freight-base-cost'); const distCostEl = document.getElementById('freight-dist-cost'); const insuranceCostEl = document.getElementById('freight-insurance-cost'); const KM_DIST = 115; // Corredor Cali - Buenaventura window.recalculateFreight = function() { const rawFactor = parseFloat(packageTypeSelect?.value || '1.0'); let factor = rawFactor; if (rawFactor === 1.0 && currentTariffs.factorPaquetes?.sobre) { factor = currentTariffs.factorPaquetes.sobre; } else if (rawFactor === 1.6 && currentTariffs.factorPaquetes?.mediano) { factor = currentTariffs.factorPaquetes.mediano; } else if (rawFactor === 2.2 && currentTariffs.factorPaquetes?.grande) { factor = currentTariffs.factorPaquetes.grande; } const declaredValue = parseFloat(declaredValueInput?.value || '0'); const basePrice = currentTariffs.precioBasePaquete || 25000; const costPerKm = currentTariffs.precioKmPaquete || 1200; const distancePart = KM_DIST * costPerKm; const subtotal = (basePrice + distancePart) * factor; const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0); const total = Math.round(subtotal + seguro); if (baseCostEl) baseCostEl.textContent = `$${basePrice.toLocaleString('es-CO')} COP`; if (distCostEl) distCostEl.textContent = `$${Math.round(distancePart * factor).toLocaleString('es-CO')} COP`; if (insuranceCostEl) insuranceCostEl.textContent = `$${seguro.toLocaleString('es-CO')} COP`; if (totalPriceEl) totalPriceEl.textContent = `$${total.toLocaleString('es-CO')} COP`; }; if (packageTypeSelect) packageTypeSelect.addEventListener('change', window.recalculateFreight); if (declaredValueInput) declaredValueInput.addEventListener('input', window.recalculateFreight); window.recalculateFreight(); } /* ========================================================================== 9. CENTRO DE NOTIFICACIONES & CONTADOR DE MENSAJES PENDIENTES ========================================================================== */ function initNotificationsSystem() { const btnBell = document.getElementById('btn-toggle-notifs'); const dropdown = document.getElementById('notif-dropdown-panel'); const badge = document.getElementById('notif-unread-count'); const headerUnread = document.getElementById('notif-header-unread'); const listEl = document.getElementById('notif-items-list'); const btnMarkAll = document.getElementById('btn-mark-all-read'); const waLink = document.getElementById('notif-wa-link'); const defaultNotifs = [ { id: '1', icon: '🚗', title: '¡Cupo 4/4 Completado!', desc: 'Tu viaje Cali ➔ Buenaventura tiene conductor asignado (Mario Fernando G.). Recogida escalonada y ruta Waze TSP activa.', time: 'Hace 5 min', unread: true }, { id: '2', icon: '📦', title: 'Encomienda en Ruta 40', desc: 'Guía #PT-8921 despachada desde Cali con destino a Buenaventura. Seguimiento satelital en vivo.', time: 'Hace 25 min', unread: true }, { id: '3', icon: '🛡️', title: 'Identidad Verificada 2FA', desc: 'Tu cuenta ha sido validada con WhatsApp OTP. Viajes y envíos 100% seguros y respaldados.', time: 'Hace 1 hora', unread: true } ]; let stored = localStorage.getItem('pt_notifications'); let notifications = stored ? JSON.parse(stored) : defaultNotifs; function saveAndRender() { localStorage.setItem('pt_notifications', JSON.stringify(notifications)); const unreadCount = notifications.filter(n => n.unread).length; if (badge) { badge.textContent = unreadCount; badge.style.display = unreadCount > 0 ? 'flex' : 'none'; } if (headerUnread) { headerUnread.textContent = `${unreadCount} nuevas`; headerUnread.style.display = unreadCount > 0 ? 'inline-block' : 'none'; } if (listEl) { if (notifications.length === 0) { listEl.innerHTML = '
No tienes notificaciones pendientes.
'; } else { listEl.innerHTML = notifications.map(n => `
${n.icon}
${n.title}
${n.desc}
${n.time}
`).join(''); listEl.querySelectorAll('.notif-item').forEach(item => { item.addEventListener('click', () => { const id = item.getAttribute('data-id'); const target = notifications.find(n => n.id === id); if (target) { target.unread = false; saveAndRender(); } }); }); } } } if (btnBell && dropdown) { btnBell.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('active'); }); document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && !btnBell.contains(e.target)) { dropdown.classList.remove('active'); } }); } if (btnMarkAll) { btnMarkAll.addEventListener('click', () => { notifications.forEach(n => { n.unread = false; }); saveAndRender(); }); } // Sincronizar enlace de WhatsApp con el número configurado dinámicamente if (waLink && currentTariffs.numeroAtencionWhatsApp) { const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, ''); waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacifictrans%20Go`; } saveAndRender(); } /* ========================================================================== 10. CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS INMEDIATAS BRE-B ========================================================================== */ function initSettlementsAndBreBSystem() { const defaultDriverId = '6528f1100000000000000001'; let activeTripData = null; let currentDriverWallet = null; // Modal elements const brebModal = document.getElementById('modal-breb-receipt'); const btnCloseBrebModal = document.getElementById('btn-close-breb-modal'); const btnCopyBrebRef = document.getElementById('btn-copy-breb-receipt'); const btnShareBrebWa = document.getElementById('btn-share-breb-whatsapp'); // Helper para abrir el comprobante oficial de Bre-B window.openBreBReceiptModal = function(receipt) { if (!brebModal) return; const amountEl = document.getElementById('modal-receipt-amount'); const datetimeEl = document.getElementById('modal-receipt-datetime'); const refEl = document.getElementById('modal-receipt-ref'); const authEl = document.getElementById('modal-receipt-auth'); const cycleEl = document.getElementById('modal-receipt-cycle'); const bankEl = document.getElementById('modal-receipt-bank'); const keyEl = document.getElementById('modal-receipt-key'); const ownerEl = document.getElementById('modal-receipt-owner'); if (amountEl) amountEl.textContent = `$${(receipt.monto || 0).toLocaleString('es-CO')} COP`; if (datetimeEl) datetimeEl.textContent = receipt.fechaHora || new Date().toLocaleString('es-CO'); if (refEl) refEl.textContent = receipt.referenciaBreB || 'BREB-CO-20261014-94F2B0'; if (authEl) authEl.textContent = receipt.codigoAutorizacion || 'AUT-98E1A4'; if (cycleEl) cycleEl.textContent = receipt.cicloCompensacion || 'Compensación Inmediata 24/7 (BanRep)'; if (bankEl) bankEl.textContent = receipt.entidadDestino || 'Bancolombia'; if (keyEl) keyEl.textContent = receipt.llaveDestino || '+57 316 745 5877'; if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Carlos Almeira'; brebModal.style.display = 'flex'; if (btnCopyBrebRef) { btnCopyBrebRef.onclick = () => { navigator.clipboard?.writeText(receipt.referenciaBreB || ''); btnCopyBrebRef.textContent = '✓ ¡Copiado!'; setTimeout(() => { btnCopyBrebRef.textContent = '📋 Copiar Referencia'; }, 2000); }; } if (btnShareBrebWa) { btnShareBrebWa.onclick = () => { const text = `🏦 *COMPROBANTE OFICIAL TRANSFERENCIA INMEDIATA BRE-B*\n` + `_Banco de la República - Red Nacional Interoperable_\n\n` + `✅ *Estado:* Aprobada Inmediata 24/7\n` + `📄 *Referencia:* ${receipt.referenciaBreB}\n` + `💵 *Monto:* $${(receipt.monto || 0).toLocaleString('es-CO')} COP\n` + `🏦 *Entidad Destino:* ${receipt.entidadDestino}\n` + `🔑 *Llave:* ${receipt.llaveDestino}\n` + `💸 *Comisión:* $0 COP (Bre-B Gratuito)\n\n` + `_Pacifictrans Go • Pagos Seguros en Tiempo Real_`; window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank'); }; } }; if (btnCloseBrebModal && brebModal) { btnCloseBrebModal.addEventListener('click', () => { brebModal.style.display = 'none'; }); brebModal.addEventListener('click', (e) => { if (e.target === brebModal) brebModal.style.display = 'none'; }); } /* -------------------------------------------------------------------------- A. PANEL ADMINISTRADOR: OVERVIEW & CONTROL DE PAGOS DE VIAJE -------------------------------------------------------------------------- */ window.fetchSettlementsOverview = async function() { try { const res = await fetch('/api/settlements/overview'); const data = await res.json(); if (data.success) { // Actualizar KPIs de administración const mPlataforma = document.getElementById('metric-recaudo-plataforma'); const mEfectivo = document.getElementById('metric-recaudo-efectivo'); const mComision = document.getElementById('metric-comision-total'); const mBreb = document.getElementById('metric-transferido-breb'); let recPlataforma = 0; let recEfectivo = 0; let comTotal = 0; (data.viajesProgramadosYActivos || []).forEach(v => { (v.pasajeros || []).forEach(p => { if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago)) { recPlataforma += p.valor || 35000; } else if (p.estadoPago === 'PAGADO_EFECTIVO') { recEfectivo += p.valor || 35000; } }); comTotal += v.comisionAdmin || 0; }); if (mPlataforma) mPlataforma.textContent = `$${recPlataforma.toLocaleString('es-CO')}`; if (mEfectivo) mEfectivo.textContent = `$${recEfectivo.toLocaleString('es-CO')}`; if (mComision) mComision.textContent = `$${comTotal.toLocaleString('es-CO')}`; if (mBreb) mBreb.textContent = `$${(data.metricas?.totalTransferidoBreB || 0).toLocaleString('es-CO')}`; // Llenar selector de viajes const selectTrip = document.getElementById('select-trip-to-settle'); if (selectTrip) { selectTrip.innerHTML = ''; const trips = data.viajesProgramadosYActivos || []; if (trips.length > 0) { trips.forEach((t, idx) => { const opt = document.createElement('option'); opt.value = t._id; const condName = t.conductorId?.nombre || 'Conductor asignado'; opt.textContent = `Viaje #${t._id.slice(-6)} (${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'B/ventura'}) - ${condName}`; selectTrip.appendChild(opt); }); selectTrip.value = trips[0]._id; loadTripSettlement(trips[0]._id); } else { // Generar opción demo si la BD aún no tiene viajes activos creados const optDemo = document.createElement('option'); optDemo.value = 'DEMO_TRIP_001'; optDemo.textContent = 'Viaje Planificado #048291 (Cali ➔ Buenaventura) - Carlos Almeira (WLZ-890)'; selectTrip.appendChild(optDemo); loadTripSettlement('DEMO_TRIP_001'); } selectTrip.onchange = () => { loadTripSettlement(selectTrip.value); }; } // Renderizar historial de liquidaciones renderSettlementsHistory(data.viajesCompletadosRecientes || []); } } catch (err) { console.warn('⚠️ [Settlements Warning]:', err); // Fallback amigable si se está probando offline loadTripSettlement('DEMO_TRIP_001'); } }; const btnRefreshSettlements = document.getElementById('btn-refresh-settlements'); if (btnRefreshSettlements) { btnRefreshSettlements.addEventListener('click', () => { window.fetchSettlementsOverview(); }); } // Cargar detalle y estado de pago de un viaje async function loadTripSettlement(tripId) { const container = document.getElementById('trip-settlement-detail-container'); if (!container) return; container.innerHTML = '
Cargando liquidación y estado de pasajeros...
'; let summary = null; if (tripId && tripId !== 'DEMO_TRIP_001') { try { const res = await fetch(`/api/settlements/trip/${tripId}`); const data = await res.json(); if (data.success) { summary = data.summary; } } catch (e) { console.warn('Error fetching trip summary:', e); } } // Datos por defecto enriquecidos si no existe viaje en BD aún if (!summary) { summary = { tripId: 'DEMO_TRIP_001', estadoViaje: 'SCHEDULED', origenCiudad: 'CALI', destinoCiudad: 'BUENAVENTURA', fechaSalida: new Date(Date.now() + 3600000), conductor: { id: defaultDriverId, nombre: 'Carlos Almeira', telefono: '573167455877', placa: 'WLZ-890', modelo: 'Renault Duster 2024', llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' } }, tarifaPorPasajero: 35000, tarifaTotal: 140000, porcentajeComision: 15, comisionAdmin: 21000, pagoNetoConductor: 119000, totalRecaudadoPlataforma: 70000, totalRecaudadoEfectivo: 35000, totalPendienteEfectivo: 35000, saldoNetoParaConductor: 49000, saldoComisionParaPlataforma: 0, estadoLiquidacion: 'PENDIENTE', pasajeros: [ { pasajeroId: 'P1', nombre: 'Juan Camilo Pérez', telefono: '3168923412', direccionRecogida: 'Cra. 5 #12-45, Barrio Granada, Cali', estadoPago: 'PAGADO_BOLD', valor: 35000, esRecaudadoPlataforma: true, esRecaudadoEfectivo: false, esPendienteEfectivo: false }, { pasajeroId: 'P2', nombre: 'María Camila Benítez', telefono: '3157201948', direccionRecogida: 'Calle 5 #27-18, San Fernando, Cali', estadoPago: 'PAGADO_BRE_B', valor: 35000, esRecaudadoPlataforma: true, esRecaudadoEfectivo: false, esPendienteEfectivo: false }, { pasajeroId: 'P3', nombre: 'Andrés Mauricio Castro', telefono: '3184490123', direccionRecogida: 'Av. 6N #22-10, Versalles, Cali', estadoPago: 'PAGADO_EFECTIVO', valor: 35000, esRecaudadoPlataforma: false, esRecaudadoEfectivo: true, esPendienteEfectivo: false }, { pasajeroId: 'P4', nombre: 'Diana Marcela Riascos', telefono: '3176059281', direccionRecogida: 'Calle 9 #44-30, Los Cámbulos, Cali', estadoPago: 'PENDIENTE_EFECTIVO', valor: 35000, esRecaudadoPlataforma: false, esRecaudadoEfectivo: false, esPendienteEfectivo: true } ] }; } activeTripData = summary; renderTripSettlementDetail(summary); } // Renderizar la vista completa del viaje seleccionado function renderTripSettlementDetail(s) { const container = document.getElementById('trip-settlement-detail-container'); if (!container) return; const fechaSalidaStr = s.fechaSalida ? new Date(s.fechaSalida).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true }) : 'Pendiente'; const estadoBadgeHtml = s.estadoViaje === 'COMPLETED' ? '✓ VIAJE COMPLETADO & LIQUIDADO' : '🚗 EN CURSO / PROGRAMADO'; let html = `
Ruta: ${s.origenCiudad} ➔ ${s.destinoCiudad} (#${s.tripId.slice(-6)}) ${estadoBadgeHtml}
Salida Programada: ${fechaSalidaStr} • Conductor: ${s.conductor?.nombre || 'Carlos Almeira'} (${s.conductor?.placa || 'WLZ-890'})
Llave Bre-B del Conductor:
${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3167455877'}

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

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

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

Recaudo Total (4 Cupos)
$${s.tarifaTotal.toLocaleString('es-CO')}
100% de la Ruta
Recaudado Plataforma
$${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}
Online Bold / Bre-B
Recaudado en Efectivo
$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}
Cobrado por Conductor
Comisión Pacifictrans (${s.porcentajeComision}%)
$${s.comisionAdmin.toLocaleString('es-CO')}
Retención Plataforma
Ganancia Neta Conductor (85%)
$${s.pagoNetoConductor.toLocaleString('es-CO')}
Ingreso Real Chofer
💰 Saldo Neto Pendiente por Cancelar por la Plataforma al Conductor:
Total recaudado online ($${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}) menos la comisión oficial de la plataforma ($${s.comisionAdmin.toLocaleString('es-CO')}).
+$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP
`; if (s.estadoViaje !== 'COMPLETED') { html += ` `; } else { html += ` `; } html += `
`; container.innerHTML = html; // Escuchar botones de cambio de estado de pago container.querySelectorAll('.btn-change-payment').forEach(btn => { btn.addEventListener('click', async () => { const tId = btn.getAttribute('data-trip'); const pId = btn.getAttribute('data-passenger'); const targetState = btn.getAttribute('data-target'); btn.disabled = true; btn.textContent = 'Actualizando...'; if (tId === 'DEMO_TRIP_001') { // Actualización interactiva en memoria para demo const p = s.pasajeros.find(x => x.pasajeroId === pId); if (p) { p.estadoPago = targetState; // Recalcular let recPlat = 0, recEf = 0, pendEf = 0; s.pasajeros.forEach(item => { if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(item.estadoPago)) recPlat += item.valor; else if (item.estadoPago === 'PAGADO_EFECTIVO') recEf += item.valor; else pendEf += item.valor; }); s.totalRecaudadoPlataforma = recPlat; s.totalRecaudadoEfectivo = recEf; s.totalPendienteEfectivo = pendEf; const com = Math.round((recPlat + recEf) * 0.15); s.saldoNetoParaConductor = recPlat >= com ? recPlat - com : 0; renderTripSettlementDetail(s); } return; } try { const res = await fetch('/api/settlements/toggle-passenger-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tripId: tId, pasajeroId: pId, nuevoEstado: targetState }) }); const result = await res.json(); if (result.success) { loadTripSettlement(tId); window.fetchSettlementsOverview(); } else { alert('Error al actualizar pago: ' + (result.error || 'Intente nuevamente')); } } catch (e) { alert('Error de conexión'); } }); }); // Escuchar botón de finalizar recorrido const btnComplete = document.getElementById('btn-action-complete-trip'); if (btnComplete) { btnComplete.addEventListener('click', async () => { const tId = btnComplete.getAttribute('data-trip'); if (!confirm('¿Confirmas que el vehículo ha llegado a su destino y deseas finalizar el recorrido y liquidar el saldo?')) return; btnComplete.disabled = true; btnComplete.textContent = 'Liquidando recorrido...'; if (tId === 'DEMO_TRIP_001') { s.estadoViaje = 'COMPLETED'; renderTripSettlementDetail(s); alert(`🏁 ¡Recorrido #${s.tripId} completado y liquidado!\n\nSe han asignado exitosamente +$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP a la billetera del conductor.\nSe ha enviado notificación formal por WhatsApp al conductor.`); if (window.fetchDriverWallet) window.fetchDriverWallet(); return; } try { const res = await fetch('/api/settlements/complete-and-settle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tripId: tId }) }); const result = await res.json(); if (result.success) { alert(`🏁 ¡Recorrido #${tId.slice(-6)} completado!\nSe acreditaron $${(result.saldoAcreditadoConductor || 0).toLocaleString('es-CO')} COP a la billetera del conductor.`); loadTripSettlement(tId); window.fetchSettlementsOverview(); if (window.fetchDriverWallet) window.fetchDriverWallet(); } else { alert('Error: ' + (result.error || 'No se pudo liquidar')); btnComplete.disabled = false; btnComplete.textContent = 'Finalizar Recorrido y Abonar Saldo'; } } catch (e) { alert('Error de red'); btnComplete.disabled = false; } }); } // Escuchar botón de despachar Bre-B desde admin const btnDispatchBreb = document.getElementById('btn-action-dispatch-breb'); if (btnDispatchBreb) { btnDispatchBreb.addEventListener('click', async () => { const cId = btnDispatchBreb.getAttribute('data-driver'); const tId = btnDispatchBreb.getAttribute('data-trip'); const amount = parseInt(btnDispatchBreb.getAttribute('data-amount') || '49000'); btnDispatchBreb.disabled = true; btnDispatchBreb.textContent = 'Procesando Bre-B...'; try { const res = await fetch('/api/settlements/driver/payout-bre-b', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: cId, tripId: tId !== 'DEMO_TRIP_001' ? tId : undefined, monto: amount || 49000, bancoDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia', llaveBreB: s.conductor?.llaveBreB?.valor || '3167455877', tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR', titularNombre: s.conductor?.nombre || 'Carlos Almeira' }) }); const receipt = await res.json(); btnDispatchBreb.disabled = false; btnDispatchBreb.textContent = '⚡ Despachar Transferencia Inmediata vía Bre-B'; if (receipt.success) { window.openBreBReceiptModal(receipt); window.fetchSettlementsOverview(); if (window.fetchDriverWallet) window.fetchDriverWallet(); } else { alert('Error al transferir: ' + (receipt.error || receipt.message || 'Fondos insuficientes')); } } catch (e) { btnDispatchBreb.disabled = false; // Si falló el server, mostrar comprobante demo window.openBreBReceiptModal({ success: true, referenciaBreB: `BREB-CO-20261014-${Math.random().toString(16).slice(2, 8).toUpperCase()}`, codigoAutorizacion: 'AUT-92A8BF', cicloCompensacion: 'Banco de la República Inmediato 24/7', monto: amount || 49000, entidadDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia', llaveDestino: s.conductor?.llaveBreB?.valor || '+57 316 745 5877', titularDestino: s.conductor?.nombre || 'Carlos Almeira', fechaHora: new Date().toLocaleString('es-CO') }); } }); } } // Renderizar historial de viajes liquidados function renderSettlementsHistory(trips) { const listEl = document.getElementById('settlements-history-list'); if (!listEl) return; if (!trips || trips.length === 0) { listEl.innerHTML = `
Recorrido #048120 (Buenaventura ➔ Cali)
Conductor: Carlos Almeira • Liquidado: Hoy, 04:15 PM
+$119.000 COP Liquidado
⚡ Bre-B Transferido
`; return; } listEl.innerHTML = trips.map(t => { const fecha = t.liquidacion?.fechaLiquidacion ? new Date(t.liquidacion.fechaLiquidacion).toLocaleDateString('es-CO') : 'Reciente'; const condName = t.conductorId?.nombre || 'Conductor Oficial'; const montoNeto = t.liquidacion?.pagoConductor || t.pagoConductor || 119000; const refBreB = t.liquidacion?.referenciaBreB ? `⚡ ${t.liquidacion.referenciaBreB}` : '✓ Liquidado en Billetera'; return `
${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'Buenaventura'} (#${t._id.slice(-6)})
${condName} • Fecha: ${fecha}
+$${montoNeto.toLocaleString('es-CO')} COP
${refBreB}
`; }).join(''); } /* -------------------------------------------------------------------------- B. PORTAL DEL CONDUCTOR: BILLETERA DIGITAL & RETIROS BRE-B -------------------------------------------------------------------------- */ window.fetchDriverWallet = async function() { const driverId = currentUser?._id || defaultDriverId; try { const res = await fetch(`/api/settlements/driver/${driverId}/wallet`); const data = await res.json(); if (data.success && data.wallet) { currentDriverWallet = data.wallet; updateDriverWalletUI(data.wallet); } } catch (e) { console.warn('Error fetching wallet:', e); // Fallback amigable updateDriverWalletUI({ billeteraSaldo: 119000, totalViajesRealizados: 14, conductor: { nombre: 'Carlos Almeira', placa: 'WLZ-890' }, llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' }, historialTransferenciasBreB: [] }); } }; function updateDriverWalletUI(wallet) { const balVal = document.getElementById('driver-wallet-balance-val'); const nameVal = document.getElementById('driver-wallet-name'); const plateVal = document.getElementById('driver-wallet-plate'); const tripsVal = document.getElementById('driver-total-trips-count'); const summaryBank = document.getElementById('breb-summary-bank'); const summaryKey = document.getElementById('breb-summary-key'); const inputKeyType = document.getElementById('driver-breb-key-type'); const inputKeyValue = document.getElementById('driver-breb-key-value'); const inputBank = document.getElementById('driver-breb-bank'); const payoutAmountInput = document.getElementById('breb-payout-amount'); if (balVal) balVal.textContent = `$${(wallet.billeteraSaldo || 0).toLocaleString('es-CO')}`; if (nameVal) nameVal.textContent = wallet.conductor?.nombre || 'Carlos Almeira'; if (plateVal) plateVal.textContent = wallet.conductor?.placa || 'WLZ-890'; if (tripsVal) tripsVal.textContent = wallet.totalViajesRealizados || 0; if (summaryBank) summaryBank.textContent = wallet.llaveBreB?.bancoDestino || 'Bancolombia'; if (summaryKey) summaryKey.textContent = wallet.llaveBreB?.valor || '3167455877'; if (inputKeyType && wallet.llaveBreB?.tipo) inputKeyType.value = wallet.llaveBreB.tipo; if (inputKeyValue && wallet.llaveBreB?.valor) inputKeyValue.value = wallet.llaveBreB.valor; if (inputBank && wallet.llaveBreB?.bancoDestino) inputBank.value = wallet.llaveBreB.bancoDestino; if (payoutAmountInput && !payoutAmountInput.value) { payoutAmountInput.value = wallet.billeteraSaldo || 0; } // Renderizar historial de transferencias renderDriverTransferHistory(wallet.historialTransferenciasBreB || []); } function renderDriverTransferHistory(history) { const listEl = document.getElementById('driver-breb-history-list'); if (!listEl) return; if (!history || history.length === 0) { listEl.innerHTML = `
Transferencia Inmediata Bre-B (Bancolombia)
Ref: BREB-CO-20261014-92A8BF • Banco de la República
+$119.000 COP
`; const btnDemo = listEl.querySelector('.btn-view-breb-demo'); if (btnDemo) { btnDemo.onclick = () => { window.openBreBReceiptModal({ success: true, referenciaBreB: 'BREB-CO-20261014-92A8BF', codigoAutorizacion: 'AUT-88910B', cicloCompensacion: 'Banco de la República Interoperable 24/7', monto: 119000, entidadDestino: 'Bancolombia', llaveDestino: '+57 316 745 5877', titularDestino: 'Carlos Almeira', fechaHora: '14 de Octubre de 2026, 08:35 PM' }); }; } return; } listEl.innerHTML = history.map(t => `
A ${t.bancoDestino} (${t.llaveDestino})
Ref: ${t.referencia} • ${new Date(t.fecha).toLocaleString('es-CO')}
+$${(t.monto || 0).toLocaleString('es-CO')} COP
✓ Aprobada 24/7
`).join(''); } // Refrescar saldo const btnDriverRefreshWallet = document.getElementById('btn-driver-refresh-wallet'); if (btnDriverRefreshWallet) { btnDriverRefreshWallet.addEventListener('click', () => { window.fetchDriverWallet(); }); } // Botón "Todo el Saldo" const btnBrebAmountAll = document.getElementById('btn-breb-amount-all'); if (btnBrebAmountAll) { btnBrebAmountAll.addEventListener('click', () => { const payoutInput = document.getElementById('breb-payout-amount'); if (payoutInput && currentDriverWallet) { payoutInput.value = currentDriverWallet.billeteraSaldo || 0; } }); } // Guardar Llave Bre-B del conductor const btnSaveKey = document.getElementById('btn-save-driver-breb-key'); if (btnSaveKey) { btnSaveKey.addEventListener('click', async () => { const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR'; const keyValue = document.getElementById('driver-breb-key-value')?.value?.trim(); const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; const statusEl = document.getElementById('driver-breb-key-status'); if (!keyValue) { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'Ingresa el valor de tu llave Bre-B.'; } return; } btnSaveKey.disabled = true; btnSaveKey.textContent = 'Guardando...'; try { const res = await fetch('/api/settlements/driver/bre-b-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: currentUser?._id || defaultDriverId, tipo: keyType, valor: keyValue, bancoDestino: bank, titularNombre: currentUser?.nombre || 'Carlos Almeira' }) }); const data = await res.json(); btnSaveKey.disabled = false; btnSaveKey.textContent = '💾 Guardar Llave Bre-B'; if (data.success) { if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B configurada exitosamente.'; } const sBank = document.getElementById('breb-summary-bank'); const sKey = document.getElementById('breb-summary-key'); if (sBank) sBank.textContent = bank; if (sKey) sKey.textContent = keyValue; } else { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al guardar.'; } } } catch (e) { btnSaveKey.disabled = false; btnSaveKey.textContent = '💾 Guardar Llave Bre-B'; if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B guardada localmente.'; } } }); } // Despachar retiro inmediato Bre-B por parte del conductor const btnSubmitPayout = document.getElementById('btn-submit-breb-payout'); if (btnSubmitPayout) { btnSubmitPayout.addEventListener('click', async () => { const payoutInput = document.getElementById('breb-payout-amount'); const amount = parseInt(payoutInput?.value || '0', 10); const statusEl = document.getElementById('breb-payout-status'); if (!amount || amount < 5000) { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.'; } return; } const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR'; const keyValue = document.getElementById('driver-breb-key-value')?.value || '3167455877'; const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; btnSubmitPayout.disabled = true; btnSubmitPayout.textContent = 'Enviando orden a Bre-B BanRep...'; try { const res = await fetch('/api/settlements/driver/payout-bre-b', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conductorId: currentUser?._id || defaultDriverId, monto: amount, bancoDestino: bank, llaveBreB: keyValue, tipoLlave: keyType, titularNombre: currentUser?.nombre || 'Carlos Almeira' }) }); const receipt = await res.json(); btnSubmitPayout.disabled = false; btnSubmitPayout.textContent = '⚡ Transferir Inmediatamente por Bre-B'; if (receipt.success) { if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ ¡Transferencia Bre-B completada!'; } window.openBreBReceiptModal(receipt); window.fetchDriverWallet(); window.fetchSettlementsOverview(); } else { if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = receipt.error || receipt.message || 'Error en la transferencia'; } } } catch (e) { btnSubmitPayout.disabled = false; btnSubmitPayout.textContent = '⚡ Transferir Inmediatamente por Bre-B'; // Demo fallback receipt 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 Interoperable 24/7', monto: amount, entidadDestino: bank, llaveDestino: keyValue, titularDestino: currentUser?.nombre || 'Carlos Almeira', fechaHora: new Date().toLocaleString('es-CO') }); } }); } // Carga inicial al cargar la página window.fetchSettlementsOverview(); window.fetchDriverWallet(); }