1043 lines
40 KiB
JavaScript
1043 lines
40 KiB
JavaScript
// 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();
|
||
initPassengerCalculator();
|
||
initFreightCalculator();
|
||
initLeafletRadarMap();
|
||
initReviewsAndStats();
|
||
initTariffsManager();
|
||
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 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';
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ==========================================================================
|
||
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: <strong>${data.codePreview}</strong>`;
|
||
}
|
||
|
||
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: `
|
||
<div class="vehicle-pin" title="${driver.nombre}">
|
||
🚗
|
||
</div>
|
||
`,
|
||
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 = `
|
||
<div class="vehicle-tooltip-card">
|
||
<div class="vehicle-tooltip-header">
|
||
<span class="vehicle-tooltip-name">#${index + 1} ${driver.nombre}</span>
|
||
<span class="vehicle-tooltip-status status-badge-${driver.estado.toLowerCase()}">● ${driver.estado}</span>
|
||
</div>
|
||
<div class="vehicle-tooltip-car">${driver.vehiculo}</div>
|
||
<div class="vehicle-tooltip-residence">🏠 Domicilio: <strong>${driver.barrio}</strong></div>
|
||
<div style="font-size: 0.75rem; color: #38bdf8;">📍 Cercanía a 1er Pasajero: ${driver.distCali}</div>
|
||
</div>
|
||
`;
|
||
|
||
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: `<div class="city-marker-pin">${label}</div>`,
|
||
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) => `
|
||
<div class="driver-card">
|
||
<div>
|
||
<strong style="color: #fff; font-size: 1.05rem;">#${index + 1} ${d.nombre}</strong>
|
||
<div style="font-size: 0.85rem; color: #94a3b8;">${d.vehiculo}</div>
|
||
<div style="font-size: 0.82rem; color: #06b6d4; margin-top: 3px;">
|
||
🏠 Residencia en Cali: <strong>${d.barrio}</strong>
|
||
</div>
|
||
<span class="driver-tag">Distancia al 1er Pasajero: ${d.distCali}</span>
|
||
</div>
|
||
<div style="text-align: right;">
|
||
<span style="display: inline-block; padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 700; background: ${d.estado === 'DISPONIBLE' ? 'rgba(16, 185, 129, 0.2)' : 'rgba(245, 158, 11, 0.2)'}; color: ${d.estado === 'DISPONIBLE' ? '#10b981' : '#f59e0b'};">
|
||
${d.estado}
|
||
</span>
|
||
<button onclick="asignarConductorDemo('${d.nombre}', '${d.barrio}')" style="margin-top: 8px; display: block; background: #0f172a; border: 1px solid rgba(6, 182, 212, 0.4); color: #06b6d4; font-size: 0.8rem; padding: 5px 12px; border-radius: 6px; cursor: pointer;">
|
||
Asignar y Programar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
window.asignarConductorDemo = function(conductor, barrio) {
|
||
const resultDiv = document.getElementById('schedule-dispatch-result');
|
||
if (resultDiv) {
|
||
resultDiv.style.display = 'block';
|
||
resultDiv.innerHTML = `
|
||
<div style="background: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.4); border-radius: 12px; padding: 1.25rem; color: #10b981;">
|
||
<h4 style="margin-bottom: 0.5rem; font-size: 1.1rem; color: #fff;">✅ Conductor Asignado por Residencia: <strong>${conductor}</strong> (${barrio})</h4>
|
||
<p style="color: #cbd5e1; font-size: 0.92rem; margin-bottom: 0.75rem;">
|
||
🚀 <strong>Disparo Inmediato de Notificación WhatsApp Ejecutado:</strong>
|
||
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.
|
||
</p>
|
||
<div style="background: rgba(0,0,0,0.4); padding: 0.75rem; border-radius: 8px; font-family: monospace; font-size: 0.85rem; color: #6ee7b7;">
|
||
📲 [Evolution API WhatsApp Status: 201 Created] Enviado a los 4 Pasajeros + Conductor.
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 => `<span class="review-tag-pill">${t}</span>`).join('');
|
||
const isPasajero = r.autorRol === 'PASAJERO';
|
||
|
||
return `
|
||
<div class="review-card">
|
||
<div>
|
||
<div class="review-card-header">
|
||
<div>
|
||
<div class="review-author">${r.autorNombre}</div>
|
||
<span class="review-role-badge ${isPasajero ? 'review-role-pasajero' : 'review-role-conductor'}">
|
||
${isPasajero ? '👤 Pasajero Verificado' : '🚗 Conductor Certificado'}
|
||
</span>
|
||
</div>
|
||
<div class="review-stars-row" title="${r.puntuacion} de 5 estrellas">${starsString}</div>
|
||
</div>
|
||
<div style="font-size: 0.78rem; color: #94a3b8; margin-bottom: 0.5rem;">
|
||
Ruta: <strong>${r.ruta || 'Cali ➔ Buenaventura'}</strong> • Calificó a: <strong style="color: #cbd5e1;">${r.destinatarioNombre}</strong>
|
||
</div>
|
||
<div class="review-text">"${r.comentario}"</div>
|
||
</div>
|
||
<div class="review-tags-list">
|
||
${tagsHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).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 = `
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
||
</svg>
|
||
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 = `✅ <strong>¡Configuración guardada y sincronizada!</strong> 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 = `✅ <strong>Configuración actualizada localmente.</strong>`;
|
||
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 = `
|
||
<svg class="animate-spin" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10" stroke-opacity="0.25"></circle>
|
||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor"></path>
|
||
</svg> 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();
|
||
}
|