+
+
+
+ 👥 Métricas Clave para Pasajeros
+
+
+
+ Tasa de Satisfacción Pasajeros:
+ 99.2% de aprobación
-
-
-
-
📦 Mensajería y Fletes Especiales
-
-
-
-
- Costo base fijo de recepción y despacho de paquetería.
-
-
-
-
-
- Multiplicador por distancia en el corredor (115 km Cali-Buenaventura).
-
-
-
-
-
+
+ Tiempo Promedio en Ruta (Vía 40):
+ 2h 15m puerta a puerta
+
+
+ Cupos de Transporte Reservados:
+ 1.536 cupos completados
+
+
+ Seguridad Calificada por Usuarios:
+ 5.0 / 5.0 (Cero siniestros)
-
-
-
- 💾 Tarifas sincronizadas con MongoDB.
+
+
+
+ 🚗 Métricas Clave para Conductores
+
+
+
+ Tasa de Ocupación por Salida:
+ 4 / 4 cupos (100% de ocupación)
-
+
+
+
+
+
+
+
Opiniones y Calificaciones de la Comunidad
+
Experiencias verificadas de pasajeros y conductores en la ruta Cali - Buenaventura.
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Conductores Sugeridos para el Viaje Actual
+
+ Ordenados según la cercanía entre su domicilio registrado en Cali y la 1ª recogida del grupo de pasajeros.
+
+
+
+
+
+
+
+
+
+
+
+
@@ -567,7 +747,7 @@
Pacifictrans Go — Transporte Especial Cali - Buenaventura
© 2026 Almeira Group. Todos los derechos reservados.
@@ -575,6 +755,217 @@
+
+
+
+
+
+
+
+ 🛡️ Seguridad y Verificación 2FA
+
+
+ Acceso Seguro con Verificación OTP
+
+
+ Para garantizar la seguridad de conductores y pasajeros, validamos tu identidad mediante código OTP por WhatsApp y correo.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
⭐
+
+ Calificar Experiencia de Viaje
+
+
+ Tu reseña ayuda a mantener los más altos estándares de calidad y seguridad en Pacifictrans Go.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📲
+
+ Instalar Pacifictrans Go en tu Móvil
+
+
+ Accede instantáneamente sin barras de navegación y con soporte offline.
+
+
+
+
+
+
+ 🍏 En iPhone / iPad (Safari)
+
+
+
+
1
+
+ Abre esta página en el navegador Safari de tu iPhone.
+
+
+
+
+
2
+
+ Toca el botón Compartir (icono de cuadrado con flecha hacia arriba ⬆️ o ⎋) en la barra inferior.
+
+
+
+
+
3
+
+ Desliza hacia abajo y selecciona "Agregar al inicio" (o "Añadir a pantalla de inicio" ➕).
+
+
+
+
+
4
+
+ Toca "Agregar" en la esquina superior derecha. ¡Listo! Quedará instalada como app nativa.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/transporte-cali-buenaventura/backend/public/js/app.js b/transporte-cali-buenaventura/backend/public/js/app.js
index 9e4585e..a265ee9 100644
--- a/transporte-cali-buenaventura/backend/public/js/app.js
+++ b/transporte-cali-buenaventura/backend/public/js/app.js
@@ -7,38 +7,773 @@ let currentTariffs = {
seguroBasePaquete: 5000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
- radioMaxPoolingMetros: 4000
+ 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();
initPassengerCalculator();
initFreightCalculator();
- initRadarSimulation();
+ initLeafletRadarMap();
+ initReviewsAndStats();
initTariffsManager();
fetchTariffs();
+ fetchStatsAndReviews();
});
-// TABS NAVIGATION
+/* ==========================================================================
+ 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');
-
- tabBtns.forEach(b => b.classList.remove('active'));
- tabContents.forEach(c => c.classList.remove('active'));
-
- btn.classList.add('active');
- const targetEl = document.getElementById(targetId);
- if (targetEl) targetEl.classList.add('active');
+ 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 viewRadar = document.getElementById('admin-view-radar');
+ const viewTarifas = document.getElementById('admin-view-tarifas');
+
+ if (subRadarBtn && subTarifasBtn && viewRadar && viewTarifas) {
+ subRadarBtn.addEventListener('click', () => {
+ subRadarBtn.classList.add('active');
+ subTarifasBtn.classList.remove('active');
+ viewRadar.style.display = 'block';
+ viewTarifas.style.display = 'none';
+ if (leafletMap) {
+ setTimeout(() => leafletMap.invalidateSize(), 200);
+ }
+ });
+
+ subTarifasBtn.addEventListener('click', () => {
+ subTarifasBtn.classList.add('active');
+ subRadarBtn.classList.remove('active');
+ viewRadar.style.display = 'none';
+ viewTarifas.style.display = 'block';
+ });
+ }
}
-// CARGA DINÁMICA DE TARIFAS DESDE EL BACKEND / MONGODB
+/* ==========================================================================
+ 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 = `
+
+ `;
+
+ 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 `
+
+
+
+
+ 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');
@@ -55,9 +790,8 @@ async function fetchTariffs() {
}
}
-// ACTUALIZAR TODOS LOS VALORES DE PRECIO EN LA INTERFAZ
function syncTariffsToUI() {
- // 1. Campos del formulario administrativo de tarifas
+ // 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');
@@ -68,6 +802,8 @@ function syncTariffsToUI() {
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;
@@ -79,8 +815,19 @@ function syncTariffsToUI() {
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. Precios en módulo de reservas de pasajeros
+ // 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;
@@ -95,13 +842,20 @@ function syncTariffsToUI() {
`;
}
- // 3. Recalcular módulo de mensajería y fletes
+ // 4. Recalcular módulo de mensajería y fletes
if (window.recalculateFreight) {
window.recalculateFreight();
}
}
-// GESTOR DE CONFIGURACIÓN DE TARIFAS (ADMIN)
+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');
@@ -128,7 +882,9 @@ function initTariffsManager() {
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')
+ 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 {
@@ -143,49 +899,45 @@ function initTariffsManager() {
currentTariffs = { ...currentTariffs, ...data.config };
syncTariffsToUI();
if (statusEl) {
- statusEl.innerHTML = `✅
¡Tarifas guardadas y sincronizadas en MongoDB! Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Base Flete: $${payload.precioBasePaquete.toLocaleString('es-CO')} COP.`;
+ 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) {
- // Fallback local visual
currentTariffs = { ...currentTariffs, ...payload };
syncTariffsToUI();
if (statusEl) {
- statusEl.innerHTML = `✅
Tarifas actualizadas en la plataforma: Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP.`;
+ statusEl.innerHTML = `✅
Configuración actualizada localmente.`;
statusEl.style.color = '#10b981';
}
} finally {
if (btn) {
btn.disabled = false;
- btn.innerHTML = '💾 Guardar y Aplicar Nuevas Tarifas';
+ btn.innerHTML = '💾 Guardar y Aplicar Configuración';
}
}
});
}
-// PASSENGER POOLING & BOOKING LOGIC
+/* ==========================================================================
+ 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');
- const poolBadge = document.getElementById('pool-status-badge');
- let currentSeatsFilled = 3; // Starts at 3/4 for instant visual demonstration
+ 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 (addressGroup) addressGroup.style.display = isCali ? 'block' : 'none';
+ if (buenaStopGroup) buenaStopGroup.style.display = isCali ? 'block' : 'none';
}
if (originSelect) {
@@ -196,6 +948,14 @@ function initPassengerCalculator() {
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 = `