// 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: ${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}
Asignar y Programar
`).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');
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();
}