almeira-web/transporte-cali-buenaventura/backend/public/js/app.js

2696 lines
113 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Pacifictrans Go - Interactive Web Platform Logic
let currentTariffs = {
tarifaPasajeroFija: 35000,
precioBasePaquete: 25000,
precioKmPaquete: 1200,
factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 },
seguroBasePaquete: 5000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
radioMaxPoolingMetros: 4000,
numeroAtencionWhatsApp: '573167455877',
nombreSoporte: 'Atención Pacifictrans Go'
};
let currentUser = null;
let leafletMap = null;
let deferredPrompt = null;
// Drivers Flota con coordenadas reales en el corredor Cali - Buenaventura
const activeDrivers = [
{ id: '1', nombre: 'Mario Fernando G.', vehiculo: 'Renault Duster (WLZ-890)', barrio: 'Granada (Comuna 2)', distCali: '1.2 km', lat: 3.4552, lng: -76.5350, estado: 'DISPONIBLE' },
{ id: '2', nombre: 'Hernán Darío P.', vehiculo: 'Toyota Fortuner (VBF-312)', barrio: 'San Antonio (Comuna 3)', distCali: '2.4 km', lat: 3.4485, lng: -76.5415, estado: 'ASIGNADO' },
{ id: '3', nombre: 'Mauricio V.', vehiculo: 'Chevrolet Tracker (HJK-456)', barrio: 'El Ingenio (Comuna 17)', distCali: '6.8 km', lat: 3.3850, lng: -76.5290, estado: 'DISPONIBLE' },
{ id: '4', nombre: 'Javier Restrepo', vehiculo: 'Nissan Kicks (RTY-789)', barrio: 'Limonar (Comuna 19)', distCali: '5.1 km', lat: 3.6580, lng: -76.6900, estado: 'EN_RUTA' }
];
document.addEventListener('DOMContentLoaded', () => {
initPWA();
initTabs();
initAuthSystem();
initNotificationsSystem();
initPassengerCalculator();
initFreightCalculator();
initLeafletRadarMap();
initReviewsAndStats();
initTariffsManager();
initSettlementsAndBreBSystem();
fetchTariffs();
fetchStatsAndReviews();
});
/* ==========================================================================
1. PWA & IPHONE INSTALLATION SYSTEM
========================================================================== */
function initPWA() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('✅ [PWA] Service Worker registrado exitosamente:', reg.scope))
.catch(err => console.warn('⚠️ [PWA Service Worker Warning]:', err));
});
}
// Guardar evento de instalación de Android
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
});
const btnInstall = document.getElementById('btn-install-pwa');
const modalInstall = document.getElementById('modal-pwa-install');
const btnClose = document.getElementById('btn-close-pwa-modal');
const btnTriggerAndroid = document.getElementById('btn-trigger-android-pwa');
if (btnInstall && modalInstall) {
btnInstall.addEventListener('click', () => {
modalInstall.classList.add('active');
});
}
if (btnClose && modalInstall) {
btnClose.addEventListener('click', () => {
modalInstall.classList.remove('active');
});
modalInstall.addEventListener('click', (e) => {
if (e.target === modalInstall) modalInstall.classList.remove('active');
});
}
if (btnTriggerAndroid) {
btnTriggerAndroid.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('PWA Outcome:', outcome);
deferredPrompt = null;
if (modalInstall) modalInstall.classList.remove('active');
} else {
alert('Para instalar en iPhone, sigue los 4 pasos mostrados arriba en Safari. En Android Chrome, toca los 3 puntos (⋮) y selecciona "Instalar aplicación".');
}
});
}
}
/* ==========================================================================
2. TABS & ADMIN SUBTABS NAVIGATION
========================================================================== */
function initTabs() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
// Switcher de Vistas Principales SPA con Escudo de Seguridad
window.showView = function(viewName) {
const viewPublic = document.getElementById('view-public-portal');
const viewAdmin = document.getElementById('view-admin-dashboard');
const viewDriver = document.getElementById('view-driver-portal');
const adminGate = document.getElementById('admin-security-gate');
const adminContent = document.getElementById('admin-console-content');
const driverGate = document.getElementById('driver-security-gate');
const driverContent = document.getElementById('driver-console-content');
// Ocultar todas las vistas principales
if (viewPublic) viewPublic.style.display = 'none';
if (viewAdmin) viewAdmin.style.display = 'none';
if (viewDriver) viewDriver.style.display = 'none';
// Deseleccionar enlaces superiores
document.querySelectorAll('.nav-links a').forEach(a => a.classList.remove('active'));
if (viewName === 'admin') {
if (viewAdmin) viewAdmin.style.display = 'block';
const isAdmin = currentUser && (currentUser.rol === 'ADMIN' || currentUser.rol === 'OPERADOR');
if (isAdmin) {
if (adminGate) adminGate.style.display = 'none';
if (adminContent) adminContent.style.display = 'block';
const adminNameEl = document.getElementById('admin-user-name');
if (adminNameEl) adminNameEl.textContent = `${currentUser.nombre} (${currentUser.rol})`;
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 250);
if (window.fetchSettlementsOverview) window.fetchSettlementsOverview();
if (window.fetchAdminPayoutRequests) window.fetchAdminPayoutRequests();
} else {
if (adminGate) adminGate.style.display = 'block';
if (adminContent) adminContent.style.display = 'none';
}
} else if (viewName === 'driver') {
if (viewDriver) viewDriver.style.display = 'block';
const isDriver = currentUser && (currentUser.rol === 'CONDUCTOR' || currentUser.rol === 'ADMIN');
if (isDriver) {
if (driverGate) driverGate.style.display = 'none';
if (driverContent) driverContent.style.display = 'block';
const driverNameEl = document.getElementById('driver-active-name');
if (driverNameEl) driverNameEl.textContent = currentUser.nombre;
if (window.fetchDriverWallet) window.fetchDriverWallet();
if (window.fetchDriverPayoutRequests) window.fetchDriverPayoutRequests();
} else {
if (driverGate) driverGate.style.display = 'block';
if (driverContent) driverContent.style.display = 'none';
}
} else {
// Default: Portal Público limpio
if (viewPublic) viewPublic.style.display = 'block';
const linkReservas = document.getElementById('nav-link-reservas');
if (linkReservas) linkReservas.classList.add('active');
}
};
// Selector elegante de servicios públicos: Pasajeros vs Encomiendas
window.activatePublicService = function(targetTabId) {
const btnPasajeros = document.getElementById('btn-service-pasajeros');
const btnFletes = document.getElementById('btn-service-fletes');
const tabPasajeros = document.getElementById('tab-pasajeros');
const tabFletes = document.getElementById('tab-fletes');
if (targetTabId === 'tab-fletes') {
if (btnPasajeros) btnPasajeros.classList.remove('active');
if (btnFletes) btnFletes.classList.add('active');
if (tabPasajeros) tabPasajeros.classList.remove('active');
if (tabFletes) tabFletes.classList.add('active');
} else {
if (btnPasajeros) btnPasajeros.classList.add('active');
if (btnFletes) btnFletes.classList.remove('active');
if (tabPasajeros) tabPasajeros.classList.add('active');
if (tabFletes) tabFletes.classList.remove('active');
}
};
const btnServicePasajeros = document.getElementById('btn-service-pasajeros');
const btnServiceFletes = document.getElementById('btn-service-fletes');
if (btnServicePasajeros) {
btnServicePasajeros.addEventListener('click', () => window.activatePublicService('tab-pasajeros'));
}
if (btnServiceFletes) {
btnServiceFletes.addEventListener('click', () => window.activatePublicService('tab-fletes'));
}
// Enlaces públicos del navbar y hero
const navLinkReservas = document.getElementById('nav-link-reservas');
if (navLinkReservas) {
navLinkReservas.addEventListener('click', (e) => {
e.preventDefault();
window.showView('public');
window.activatePublicService('tab-pasajeros');
document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' });
});
}
const navLinkFletes = document.getElementById('nav-link-fletes');
const heroBtnFlete = document.getElementById('hero-btn-flete');
[navLinkFletes, heroBtnFlete].forEach(el => {
if (el) {
el.addEventListener('click', (e) => {
e.preventDefault();
window.showView('public');
window.activatePublicService('tab-fletes');
document.getElementById('reservas')?.scrollIntoView({ behavior: 'smooth' });
});
}
});
const navLinkReviews = document.getElementById('nav-link-reviews');
if (navLinkReviews) {
navLinkReviews.addEventListener('click', (e) => {
e.preventDefault();
window.showView('public');
document.getElementById('estadisticas')?.scrollIntoView({ behavior: 'smooth' });
});
}
// Botones de acceso exclusivo para Roles en Navbar
const navBtnOpenAdmin = document.getElementById('nav-btn-open-admin');
if (navBtnOpenAdmin) {
navBtnOpenAdmin.addEventListener('click', (e) => {
e.preventDefault();
window.location.hash = '#admin';
window.showView('admin');
});
}
const navBtnOpenDriver = document.getElementById('nav-btn-open-driver');
if (navBtnOpenDriver) {
navBtnOpenDriver.addEventListener('click', (e) => {
e.preventDefault();
window.location.hash = '#conductor';
window.showView('driver');
});
}
// Botones de regreso al sitio público
const btnAdminViewPublic = document.getElementById('btn-admin-view-public');
const btnDriverViewPublic = document.getElementById('btn-driver-view-public');
const btnGateBackPublic = document.getElementById('btn-gate-back-public');
const btnGateDriverBack = document.getElementById('btn-gate-driver-back-public');
[btnAdminViewPublic, btnDriverViewPublic, btnGateBackPublic, btnGateDriverBack].forEach(b => {
if (b) {
b.addEventListener('click', () => {
window.location.hash = '#reservas';
window.showView('public');
});
}
});
// Botones para abrir modal de login desde los escudos de seguridad
const btnGateAdminLogin = document.getElementById('btn-gate-admin-login');
const btnGateDriverLogin = document.getElementById('btn-gate-driver-login');
const authModal = document.getElementById('modal-auth-dripzone');
[btnGateAdminLogin, btnGateDriverLogin].forEach(b => {
if (b && authModal) {
b.addEventListener('click', () => authModal.classList.add('active'));
}
});
const btnAdminLogout = document.getElementById('btn-admin-logout');
if (btnAdminLogout) {
btnAdminLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
currentUser = null;
if (window.updateUserUI) window.updateUserUI(null);
window.location.hash = '#reservas';
window.showView('public');
});
}
// Compatibilidad genérica para pestañas
window.activateTab = function(targetId) {
tabBtns.forEach(b => {
if (b.getAttribute('data-tab') === targetId) {
b.classList.add('active');
} else {
b.classList.remove('active');
}
});
tabContents.forEach(c => {
if (c.id === targetId) {
c.classList.add('active');
} else {
c.classList.remove('active');
}
});
if (targetId === 'tab-admin' && leafletMap) {
setTimeout(() => {
leafletMap.invalidateSize();
}, 250);
}
};
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetId = btn.getAttribute('data-tab');
window.activateTab(targetId);
});
});
// Sub-tabs de la sección administrativa
const subRadarBtn = document.getElementById('btn-admin-subtab-radar');
const subTarifasBtn = document.getElementById('btn-admin-subtab-tarifas');
const subLiquidacionesBtn = document.getElementById('btn-admin-subtab-liquidaciones');
const viewRadar = document.getElementById('admin-view-radar');
const viewTarifas = document.getElementById('admin-view-tarifas');
const viewLiquidaciones = document.getElementById('admin-view-liquidaciones');
function setAdminSubTab(activeBtn, activeView) {
[subRadarBtn, subTarifasBtn, subLiquidacionesBtn].forEach(b => b?.classList.remove('active'));
[viewRadar, viewTarifas, viewLiquidaciones].forEach(v => { if (v) v.style.display = 'none'; });
if (activeBtn) activeBtn.classList.add('active');
if (activeView) activeView.style.display = 'block';
}
if (subRadarBtn && viewRadar) {
subRadarBtn.addEventListener('click', () => {
setAdminSubTab(subRadarBtn, viewRadar);
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 200);
});
}
if (subTarifasBtn && viewTarifas) {
subTarifasBtn.addEventListener('click', () => {
setAdminSubTab(subTarifasBtn, viewTarifas);
});
}
if (subLiquidacionesBtn && viewLiquidaciones) {
subLiquidacionesBtn.addEventListener('click', () => {
setAdminSubTab(subLiquidacionesBtn, viewLiquidaciones);
if (window.fetchSettlementsOverview) window.fetchSettlementsOverview();
});
}
// Sub-tabs de la sección del conductor
const subDriverWalletBtn = document.getElementById('btn-driver-subtab-billetera');
const subDriverRegistroBtn = document.getElementById('btn-driver-subtab-registro');
const viewDriverWallet = document.getElementById('driver-view-billetera');
const viewDriverRegistro = document.getElementById('driver-view-registro');
if (subDriverWalletBtn && subDriverRegistroBtn && viewDriverWallet && viewDriverRegistro) {
subDriverWalletBtn.addEventListener('click', () => {
subDriverWalletBtn.classList.add('active');
subDriverRegistroBtn.classList.remove('active');
viewDriverWallet.style.display = 'block';
viewDriverRegistro.style.display = 'none';
if (window.fetchDriverWallet) window.fetchDriverWallet();
});
subDriverRegistroBtn.addEventListener('click', () => {
subDriverRegistroBtn.classList.add('active');
subDriverWalletBtn.classList.remove('active');
viewDriverWallet.style.display = 'none';
viewDriverRegistro.style.display = 'block';
});
}
// Enrutamiento por Hash en la URL (#admin, #conductor, etc.)
function handleUrlHash() {
const hash = (window.location.hash || '').toLowerCase();
if (hash === '#admin') {
window.showView('admin');
} else if (hash === '#conductor' || hash === '#driver') {
window.showView('driver');
} else {
window.showView('public');
if (hash === '#fletes') {
window.activatePublicService('tab-fletes');
}
}
}
window.addEventListener('hashchange', handleUrlHash);
handleUrlHash();
}
/* ==========================================================================
3. AUTHENTICATION (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'));
});
}
// Selector Rápido de Roles para Auditoría y Pruebas
document.querySelectorAll('.btn-role-quick-login').forEach(btn => {
btn.addEventListener('click', async () => {
const role = btn.getAttribute('data-role') || 'ADMIN';
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '⏳ Accediendo...';
try {
const res = await fetch('/api/auth/quick-role-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role })
});
const data = await res.json();
if (data.success && data.user) {
localStorage.setItem('pt_token', data.token);
currentUser = data.user;
updateUserUI(currentUser);
if (modal) modal.classList.remove('active');
if (role === 'ADMIN') {
window.location.hash = '#admin';
window.showView('admin');
} else if (role === 'CONDUCTOR') {
window.location.hash = '#conductor';
window.showView('driver');
} else {
window.location.hash = '#reservas';
window.showView('public');
}
} else {
alert('Error de acceso rápido: ' + (data.error || 'Intenta de nuevo'));
}
} catch (e) {
console.error('Quick role error:', e);
alert('Error de conexión al servidor');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
});
// Cerrar sesión
if (btnLogout) {
btnLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
currentUser = null;
updateUserUI(null);
window.location.hash = '#reservas';
window.showView('public');
});
}
// 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 userRoleBadge = document.getElementById('user-role-badge');
const navBtnAdmin = document.getElementById('nav-btn-open-admin');
const navBtnDriver = document.getElementById('nav-btn-open-driver');
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];
if (userRoleBadge) userRoleBadge.textContent = `✅ ${user.rol}`;
// Mostrar accesos a consolas según rol del usuario
if (navBtnAdmin) {
navBtnAdmin.style.display = (user.rol === 'ADMIN' || user.rol === 'OPERADOR') ? 'inline-flex' : 'none';
}
if (navBtnDriver) {
navBtnDriver.style.display = (user.rol === 'CONDUCTOR' || user.rol === 'ADMIN') ? 'inline-flex' : 'none';
}
// 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;
// Actualizar vista actual si está en zona protegida
const hash = (window.location.hash || '').toLowerCase();
if (hash === '#admin') window.showView('admin');
if (hash === '#conductor' || hash === '#driver') window.showView('driver');
} else {
if (btnAuthOpen) btnAuthOpen.style.display = 'inline-flex';
if (userBadge) userBadge.style.display = 'none';
if (navBtnAdmin) navBtnAdmin.style.display = 'none';
if (navBtnDriver) navBtnDriver.style.display = 'none';
const hash = (window.location.hash || '').toLowerCase();
if (hash === '#admin' || hash === '#conductor' || hash === '#driver') {
window.showView(hash === '#admin' ? 'admin' : 'driver');
}
}
}
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 canales oficiales
const elPasajero = document.getElementById('cfg-tarifa-pasajero');
const elBasePaquete = document.getElementById('cfg-base-paquete');
const elKmPaquete = document.getElementById('cfg-km-paquete');
const elSobre = document.getElementById('cfg-factor-sobre');
const elMediano = document.getElementById('cfg-factor-mediano');
const elGrande = document.getElementById('cfg-factor-grande');
const elSeguro = document.getElementById('cfg-seguro-base');
const elComision = document.getElementById('cfg-comision');
const elPenalidad = document.getElementById('cfg-penalidad');
const elRadio = document.getElementById('cfg-radio-pooling');
const elPhone = document.getElementById('cfg-admin-phone');
const elName = document.getElementById('cfg-admin-name');
// Canales de WhatsApp y separación de grupos por App
const elGroupPacific = document.getElementById('cfg-group-pacifictrans');
const elGroupDripzone = document.getElementById('cfg-group-dripzone');
const adminWaGroupBadge = document.getElementById('admin-wa-group-badge');
if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija;
if (elBasePaquete) elBasePaquete.value = currentTariffs.precioBasePaquete;
if (elKmPaquete) elKmPaquete.value = currentTariffs.precioKmPaquete;
if (elSobre && currentTariffs.factorPaquetes) elSobre.value = currentTariffs.factorPaquetes.sobre || 1.0;
if (elMediano && currentTariffs.factorPaquetes) elMediano.value = currentTariffs.factorPaquetes.mediano || 1.6;
if (elGrande && currentTariffs.factorPaquetes) elGrande.value = currentTariffs.factorPaquetes.grande || 2.2;
if (elSeguro) elSeguro.value = currentTariffs.seguroBasePaquete;
if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin;
if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion;
if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros;
if (elPhone && currentTariffs.numeroAtencionWhatsApp) elPhone.value = currentTariffs.numeroAtencionWhatsApp;
if (elName && currentTariffs.nombreSoporte) elName.value = currentTariffs.nombreSoporte;
const pacificGroup = currentTariffs.grupoWhatsAppPacificTrans || 'Pacific Trans Notificaciones';
const dripGroup = currentTariffs.grupoWhatsAppDripzone || 'Dripzone Notificaciones';
if (elGroupPacific) elGroupPacific.value = pacificGroup;
if (elGroupDripzone) elGroupDripzone.value = dripGroup;
if (adminWaGroupBadge) adminWaGroupBadge.textContent = `👥 Grupo: ${pacificGroup}`;
// 2. Sincronización del enlace dinámico de atención WhatsApp en el footer y navbar
const phoneFormatted = formatPhoneNumber(currentTariffs.numeroAtencionWhatsApp || '573167455877');
const cleanPhone = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, '');
const footerPhoneEl = document.getElementById('footer-support-phone');
const footerLinkEl = document.getElementById('footer-support-link');
if (footerPhoneEl) footerPhoneEl.textContent = phoneFormatted;
if (footerLinkEl) footerLinkEl.href = `https://wa.me/${cleanPhone}?text=Hola%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');
const btnDetect = document.getElementById('btn-detect-wa-groups');
// Auto-Detección de Grupos WhatsApp mediante Evolution API
if (btnDetect) {
btnDetect.addEventListener('click', async () => {
btnDetect.disabled = true;
const originalText = btnDetect.innerHTML;
btnDetect.innerHTML = '⏳ Detectando grupos...';
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/admin/whatsapp/groups', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await res.json();
if (res.ok && data.success) {
const inputPacific = document.getElementById('cfg-group-pacifictrans');
const inputDrip = document.getElementById('cfg-group-dripzone');
const badgePacific = document.getElementById('badge-pacific-jid');
const badgeDrip = document.getElementById('badge-dripzone-jid');
if (inputPacific && data.pacificTrans?.groupName) {
inputPacific.value = data.pacificTrans.groupName;
}
if (badgePacific && data.pacificTrans?.resolvedJid) {
badgePacific.innerHTML = `✓ Destino: <strong>${data.pacificTrans.resolvedJid}</strong> (${data.pacificTrans.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`;
badgePacific.style.color = '#34d399';
}
if (inputDrip && data.dripzone?.groupName) {
inputDrip.value = data.dripzone.groupName;
}
if (badgeDrip && data.dripzone?.resolvedJid) {
badgeDrip.innerHTML = `✓ Destino: <strong>${data.dripzone.resolvedJid}</strong> (${data.dripzone.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`;
badgeDrip.style.color = '#34d399';
}
alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacifictrans Go ➔ ${data.pacificTrans?.groupName} (${data.pacificTrans?.resolvedJid})\n• Dripzone ➔ ${data.dripzone?.groupName} (${data.dripzone?.resolvedJid})`);
} else {
alert('No se pudieron consultar los grupos: ' + (data.error || 'Verifica la sesión'));
}
} catch (err) {
console.error('Error detect groups:', err);
alert('Error al consultar Evolution API');
} finally {
btnDetect.disabled = false;
btnDetect.innerHTML = originalText;
}
});
}
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btn-save-tariffs');
if (btn) {
btn.disabled = true;
btn.innerHTML = '⏳ Guardando en MongoDB...';
}
const payload = {
tarifaPasajeroFija: parseFloat(document.getElementById('cfg-tarifa-pasajero')?.value || '35000'),
precioBasePaquete: parseFloat(document.getElementById('cfg-base-paquete')?.value || '25000'),
precioKmPaquete: parseFloat(document.getElementById('cfg-km-paquete')?.value || '1200'),
factorPaquetes: {
sobre: parseFloat(document.getElementById('cfg-factor-sobre')?.value || '1.0'),
mediano: parseFloat(document.getElementById('cfg-factor-mediano')?.value || '1.6'),
grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2')
},
seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'),
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',
grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans Notificaciones',
grupoWhatsAppDripzone: document.getElementById('cfg-group-dripzone')?.value.trim() || 'Dripzone Notificaciones'
};
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/tariffs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (res.ok) {
const data = await res.json();
currentTariffs = { ...currentTariffs, ...data.config };
syncTariffsToUI();
if (statusEl) {
statusEl.innerHTML = `✅ <strong>¡Configuración guardada y sincronizada!</strong> Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Grupo Pacific: "${payload.grupoWhatsAppPacificTrans}" | Grupo Dripzone: "${payload.grupoWhatsAppDripzone}"`;
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();
}
/* ==========================================================================
9. CENTRO DE NOTIFICACIONES & CONTADOR DE MENSAJES PENDIENTES
========================================================================== */
function initNotificationsSystem() {
const btnBell = document.getElementById('btn-toggle-notifs');
const dropdown = document.getElementById('notif-dropdown-panel');
const badge = document.getElementById('notif-unread-count');
const headerUnread = document.getElementById('notif-header-unread');
const listEl = document.getElementById('notif-items-list');
const btnMarkAll = document.getElementById('btn-mark-all-read');
const waLink = document.getElementById('notif-wa-link');
const defaultNotifs = [
{
id: '1',
icon: '🚗',
title: '¡Cupo 4/4 Completado!',
desc: 'Tu viaje Cali ➔ Buenaventura tiene conductor asignado (Mario Fernando G.). Recogida escalonada y ruta Waze TSP activa.',
time: 'Hace 5 min',
unread: true
},
{
id: '2',
icon: '📦',
title: 'Encomienda en Ruta 40',
desc: 'Guía #PT-8921 despachada desde Cali con destino a Buenaventura. Seguimiento satelital en vivo.',
time: 'Hace 25 min',
unread: true
},
{
id: '3',
icon: '🛡️',
title: 'Identidad Verificada 2FA',
desc: 'Tu cuenta ha sido validada con WhatsApp OTP. Viajes y envíos 100% seguros y respaldados.',
time: 'Hace 1 hora',
unread: true
}
];
let stored = localStorage.getItem('pt_notifications');
let notifications = stored ? JSON.parse(stored) : defaultNotifs;
function saveAndRender() {
localStorage.setItem('pt_notifications', JSON.stringify(notifications));
const unreadCount = notifications.filter(n => n.unread).length;
if (badge) {
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? 'flex' : 'none';
}
if (headerUnread) {
headerUnread.textContent = `${unreadCount} nuevas`;
headerUnread.style.display = unreadCount > 0 ? 'inline-block' : 'none';
}
if (listEl) {
if (notifications.length === 0) {
listEl.innerHTML = '<div style="padding: 1.5rem; text-align: center; color: #64748b; font-size: 0.88rem;">No tienes notificaciones pendientes.</div>';
} else {
listEl.innerHTML = notifications.map(n => `
<div class="notif-item ${n.unread ? 'unread' : ''}" data-id="${n.id}">
<div class="notif-item-icon">${n.icon}</div>
<div class="notif-item-content">
<div class="notif-item-title">${n.title}</div>
<div class="notif-item-desc">${n.desc}</div>
<div class="notif-item-time">${n.time}</div>
</div>
</div>
`).join('');
listEl.querySelectorAll('.notif-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.getAttribute('data-id');
const target = notifications.find(n => n.id === id);
if (target) {
target.unread = false;
saveAndRender();
}
});
});
}
}
}
if (btnBell && dropdown) {
btnBell.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.classList.toggle('active');
});
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target) && !btnBell.contains(e.target)) {
dropdown.classList.remove('active');
}
});
}
if (btnMarkAll) {
btnMarkAll.addEventListener('click', () => {
notifications.forEach(n => { n.unread = false; });
saveAndRender();
});
}
// Sincronizar enlace de WhatsApp con el número configurado dinámicamente
if (waLink && currentTariffs.numeroAtencionWhatsApp) {
const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, '');
waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacifictrans%20Go`;
}
saveAndRender();
}
/* ==========================================================================
10. CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS INMEDIATAS BRE-B
========================================================================== */
function initSettlementsAndBreBSystem() {
const defaultDriverId = '6528f1100000000000000001';
let activeTripData = null;
let currentDriverWallet = null;
// Modal elements
const brebModal = document.getElementById('modal-breb-receipt');
const btnCloseBrebModal = document.getElementById('btn-close-breb-modal');
const btnCopyBrebRef = document.getElementById('btn-copy-breb-receipt');
const btnShareBrebWa = document.getElementById('btn-share-breb-whatsapp');
// Helper para abrir el comprobante oficial de Bre-B
window.openBreBReceiptModal = function(receipt) {
if (!brebModal) return;
const amountEl = document.getElementById('modal-receipt-amount');
const datetimeEl = document.getElementById('modal-receipt-datetime');
const refEl = document.getElementById('modal-receipt-ref');
const authEl = document.getElementById('modal-receipt-auth');
const cycleEl = document.getElementById('modal-receipt-cycle');
const bankEl = document.getElementById('modal-receipt-bank');
const keyEl = document.getElementById('modal-receipt-key');
const ownerEl = document.getElementById('modal-receipt-owner');
if (amountEl) amountEl.textContent = `$${(receipt.monto || 0).toLocaleString('es-CO')} COP`;
if (datetimeEl) datetimeEl.textContent = receipt.fechaHora || new Date().toLocaleString('es-CO');
if (refEl) refEl.textContent = receipt.referenciaBreB || 'BREB-CO-20261014-94F2B0';
if (authEl) authEl.textContent = receipt.codigoAutorizacion || 'AUT-98E1A4';
if (cycleEl) cycleEl.textContent = receipt.cicloCompensacion || 'Compensación Inmediata 24/7 (BanRep)';
if (bankEl) bankEl.textContent = receipt.entidadDestino || 'Bancolombia';
if (keyEl) keyEl.textContent = receipt.llaveDestino || '+57 316 745 5877';
if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Carlos Almeira';
brebModal.style.display = 'flex';
if (btnCopyBrebRef) {
btnCopyBrebRef.onclick = () => {
navigator.clipboard?.writeText(receipt.referenciaBreB || '');
btnCopyBrebRef.textContent = '✓ ¡Copiado!';
setTimeout(() => { btnCopyBrebRef.textContent = '📋 Copiar Referencia'; }, 2000);
};
}
if (btnShareBrebWa) {
btnShareBrebWa.onclick = () => {
const text =
`🏦 *COMPROBANTE OFICIAL TRANSFERENCIA INMEDIATA BRE-B*\n` +
`_Banco de la República - Red Nacional Interoperable_\n\n` +
`✅ *Estado:* Aprobada Inmediata 24/7\n` +
`📄 *Referencia:* ${receipt.referenciaBreB}\n` +
`💵 *Monto:* $${(receipt.monto || 0).toLocaleString('es-CO')} COP\n` +
`🏦 *Entidad Destino:* ${receipt.entidadDestino}\n` +
`🔑 *Llave:* ${receipt.llaveDestino}\n` +
`💸 *Comisión:* $0 COP (Bre-B Gratuito)\n\n` +
`_Pacifictrans Go • Pagos Seguros en Tiempo Real_`;
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank');
};
}
};
if (btnCloseBrebModal && brebModal) {
btnCloseBrebModal.addEventListener('click', () => {
brebModal.style.display = 'none';
});
brebModal.addEventListener('click', (e) => {
if (e.target === brebModal) brebModal.style.display = 'none';
});
}
/* --------------------------------------------------------------------------
A. PANEL ADMINISTRADOR: OVERVIEW & CONTROL DE PAGOS DE VIAJE
-------------------------------------------------------------------------- */
window.fetchSettlementsOverview = async function() {
try {
const res = await fetch('/api/settlements/overview');
const data = await res.json();
if (data.success) {
// Actualizar KPIs de administración
const mPlataforma = document.getElementById('metric-recaudo-plataforma');
const mEfectivo = document.getElementById('metric-recaudo-efectivo');
const mComision = document.getElementById('metric-comision-total');
const mBreb = document.getElementById('metric-transferido-breb');
let recPlataforma = 0;
let recEfectivo = 0;
let comTotal = 0;
(data.viajesProgramadosYActivos || []).forEach(v => {
(v.pasajeros || []).forEach(p => {
if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago)) {
recPlataforma += p.valor || 35000;
} else if (p.estadoPago === 'PAGADO_EFECTIVO') {
recEfectivo += p.valor || 35000;
}
});
comTotal += v.comisionAdmin || 0;
});
if (mPlataforma) mPlataforma.textContent = `$${recPlataforma.toLocaleString('es-CO')}`;
if (mEfectivo) mEfectivo.textContent = `$${recEfectivo.toLocaleString('es-CO')}`;
if (mComision) mComision.textContent = `$${comTotal.toLocaleString('es-CO')}`;
if (mBreb) mBreb.textContent = `$${(data.metricas?.totalTransferidoBreB || 0).toLocaleString('es-CO')}`;
// Llenar selector de viajes
const selectTrip = document.getElementById('select-trip-to-settle');
if (selectTrip) {
selectTrip.innerHTML = '';
const trips = data.viajesProgramadosYActivos || [];
if (trips.length > 0) {
trips.forEach((t, idx) => {
const opt = document.createElement('option');
opt.value = t._id;
const condName = t.conductorId?.nombre || 'Conductor asignado';
opt.textContent = `Viaje #${t._id.slice(-6)} (${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'B/ventura'}) - ${condName}`;
selectTrip.appendChild(opt);
});
selectTrip.value = trips[0]._id;
loadTripSettlement(trips[0]._id);
} else {
// Generar opción demo si la BD aún no tiene viajes activos creados
const optDemo = document.createElement('option');
optDemo.value = 'DEMO_TRIP_001';
optDemo.textContent = 'Viaje Planificado #048291 (Cali ➔ Buenaventura) - Carlos Almeira (WLZ-890)';
selectTrip.appendChild(optDemo);
loadTripSettlement('DEMO_TRIP_001');
}
selectTrip.onchange = () => {
loadTripSettlement(selectTrip.value);
};
}
// Renderizar historial de liquidaciones
renderSettlementsHistory(data.viajesCompletadosRecientes || []);
}
} catch (err) {
console.warn('⚠️ [Settlements Warning]:', err);
// Fallback amigable si se está probando offline
loadTripSettlement('DEMO_TRIP_001');
}
};
const btnRefreshSettlements = document.getElementById('btn-refresh-settlements');
if (btnRefreshSettlements) {
btnRefreshSettlements.addEventListener('click', () => {
window.fetchSettlementsOverview();
});
}
// Cargar detalle y estado de pago de un viaje
async function loadTripSettlement(tripId) {
const container = document.getElementById('trip-settlement-detail-container');
if (!container) return;
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: #94a3b8;">Cargando liquidación y estado de pasajeros...</div>';
let summary = null;
if (tripId && tripId !== 'DEMO_TRIP_001') {
try {
const res = await fetch(`/api/settlements/trip/${tripId}`);
const data = await res.json();
if (data.success) {
summary = data.summary;
}
} catch (e) {
console.warn('Error fetching trip summary:', e);
}
}
// Datos por defecto enriquecidos si no existe viaje en BD aún
if (!summary) {
summary = {
tripId: 'DEMO_TRIP_001',
estadoViaje: 'SCHEDULED',
origenCiudad: 'CALI',
destinoCiudad: 'BUENAVENTURA',
fechaSalida: new Date(Date.now() + 3600000),
conductor: {
id: defaultDriverId,
nombre: 'Carlos Almeira',
telefono: '573167455877',
placa: 'WLZ-890',
modelo: 'Renault Duster 2024',
llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' }
},
tarifaPorPasajero: 35000,
tarifaTotal: 140000,
porcentajeComision: 15,
comisionAdmin: 21000,
pagoNetoConductor: 119000,
totalRecaudadoPlataforma: 70000,
totalRecaudadoEfectivo: 35000,
totalPendienteEfectivo: 35000,
saldoNetoParaConductor: 49000,
saldoComisionParaPlataforma: 0,
estadoLiquidacion: 'PENDIENTE',
pasajeros: [
{
pasajeroId: 'P1',
nombre: 'Juan Camilo Pérez',
telefono: '3168923412',
direccionRecogida: 'Cra. 5 #12-45, Barrio Granada, Cali',
estadoPago: 'PAGADO_BOLD',
valor: 35000,
esRecaudadoPlataforma: true,
esRecaudadoEfectivo: false,
esPendienteEfectivo: false
},
{
pasajeroId: 'P2',
nombre: 'María Camila Benítez',
telefono: '3157201948',
direccionRecogida: 'Calle 5 #27-18, San Fernando, Cali',
estadoPago: 'PAGADO_BRE_B',
valor: 35000,
esRecaudadoPlataforma: true,
esRecaudadoEfectivo: false,
esPendienteEfectivo: false
},
{
pasajeroId: 'P3',
nombre: 'Andrés Mauricio Castro',
telefono: '3184490123',
direccionRecogida: 'Av. 6N #22-10, Versalles, Cali',
estadoPago: 'PAGADO_EFECTIVO',
valor: 35000,
esRecaudadoPlataforma: false,
esRecaudadoEfectivo: true,
esPendienteEfectivo: false
},
{
pasajeroId: 'P4',
nombre: 'Diana Marcela Riascos',
telefono: '3176059281',
direccionRecogida: 'Calle 9 #44-30, Los Cámbulos, Cali',
estadoPago: 'PENDIENTE_EFECTIVO',
valor: 35000,
esRecaudadoPlataforma: false,
esRecaudadoEfectivo: false,
esPendienteEfectivo: true
}
]
};
}
activeTripData = summary;
renderTripSettlementDetail(summary);
}
// Renderizar la vista completa del viaje seleccionado
function renderTripSettlementDetail(s) {
const container = document.getElementById('trip-settlement-detail-container');
if (!container) return;
const fechaSalidaStr = s.fechaSalida
? new Date(s.fechaSalida).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true })
: 'Pendiente';
const estadoBadgeHtml = s.estadoViaje === 'COMPLETED'
? '<span style="background: rgba(16,185,129,0.2); color: #34d399; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.8rem; font-weight: 700;">✓ VIAJE COMPLETADO & LIQUIDADO</span>'
: '<span style="background: rgba(6,182,212,0.2); color: #38bdf8; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.8rem; font-weight: 700;">🚗 EN CURSO / PROGRAMADO</span>';
let html = `
<!-- ENCABEZADO DEL VIAJE -->
<div style="background: rgba(11, 17, 32, 0.9); border: 1px solid var(--border-glass); border-radius: 10px; padding: 1.25rem; margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div>
<div style="display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.25rem;">
<strong style="color: #fff; font-size: 1.1rem;">Ruta: ${s.origenCiudad} ➔ ${s.destinoCiudad} (#${s.tripId.slice(-6)})</strong>
${estadoBadgeHtml}
</div>
<div style="font-size: 0.85rem; color: #94a3b8;">
Salida Programada: <strong style="color: #fff;">${fechaSalidaStr}</strong> • Conductor: <strong style="color: var(--accent-cyan);">${s.conductor?.nombre || 'Carlos Almeira'} (${s.conductor?.placa || 'WLZ-890'})</strong>
</div>
</div>
<div style="font-size: 0.85rem; text-align: right;">
<div style="color: #cbd5e1;">Llave Bre-B del Conductor:</div>
<strong style="color: #ec4899;">${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3167455877'}</strong>
</div>
</div>
<!-- LISTADO DE PASAJEROS Y ESTADO DE PAGO -->
<h4 style="color: #fff; font-size: 1rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
<span>👥</span> Control de Pasajeros & Recaudos Individuales (Cupo 4/4)
</h4>
<div style="margin-bottom: 1.5rem;">
`;
s.pasajeros.forEach((p, idx) => {
let badgeHtml = '';
let actionBtnHtml = '';
if (p.estadoPago === 'PAGADO_BOLD') {
badgeHtml = '<span class="badge-payment badge-payment-online">💳 Pagado Bold</span>';
} else if (p.estadoPago === 'PAGADO_NEQUI') {
badgeHtml = '<span class="badge-payment badge-payment-online">📱 Pagado Nequi</span>';
} else if (p.estadoPago === 'PAGADO_BRE_B') {
badgeHtml = '<span class="badge-payment badge-payment-breb">⚡ Pagado Bre-B</span>';
} else if (p.estadoPago === 'PAGADO_EFECTIVO') {
badgeHtml = '<span class="badge-payment badge-payment-cash-paid">✓ Pagado en Efectivo</span>';
actionBtnHtml = `
<button type="button" class="btn-toggle-payment revert btn-change-payment" data-trip="${s.tripId}" data-passenger="${p.pasajeroId}" data-target="PENDIENTE_EFECTIVO">
↩️ Revertir a Pendiente
</button>
`;
} else {
badgeHtml = '<span class="badge-payment badge-payment-cash-pending">⏳ Pendiente Efectivo al Abordar</span>';
actionBtnHtml = `
<button type="button" class="btn-toggle-payment btn-change-payment" data-trip="${s.tripId}" data-passenger="${p.pasajeroId}" data-target="PAGADO_EFECTIVO">
💵 Confirmar Pago Efectivo Recibido
</button>
`;
}
html += `
<div class="passenger-payment-card ${p.estadoPago === 'PENDIENTE_EFECTIVO' ? 'pending-cash' : (p.estadoPago === 'PAGADO_EFECTIVO' ? 'paid-cash' : 'paid-online')}">
<div style="flex: 1; min-width: 250px;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">
<strong style="color: #fff; font-size: 0.95rem;">${idx + 1}. ${p.nombre}</strong>
<span style="color: #64748b; font-size: 0.8rem;">(+${p.telefono})</span>
</div>
<div style="font-size: 0.82rem; color: #94a3b8;">
📍 Recogida: ${p.direccionRecogida}
</div>
</div>
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
<div style="text-align: right;">
<div style="font-size: 0.78rem; color: #94a3b8;">Tarifa Cupo</div>
<strong style="color: #fff; font-size: 1rem;">$${(p.valor || 35000).toLocaleString('es-CO')}</strong>
</div>
<div>${badgeHtml}</div>
<div>${actionBtnHtml}</div>
</div>
</div>
`;
});
html += `
</div>
<!-- BALANCE FINANCIERO Y LIQUIDACIÓN -->
<div style="background: rgba(15, 23, 42, 0.95); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem;">
<h4 style="color: #fff; font-size: 1.05rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<span>⚖️</span> Liquidación Financiera del Recorrido & Split Conductor/Plataforma
</h4>
<div class="financial-summary-grid" style="margin-bottom: 1rem;">
<div class="fin-metric-card">
<div class="fin-metric-label">Recaudo Total (4 Cupos)</div>
<div class="fin-metric-val" style="color: #fff;">$${s.tarifaTotal.toLocaleString('es-CO')}</div>
<span style="font-size: 0.72rem; color: #94a3b8;">100% de la Ruta</span>
</div>
<div class="fin-metric-card">
<div class="fin-metric-label">Recaudado Plataforma</div>
<div class="fin-metric-val" style="color: #38bdf8;">$${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}</div>
<span style="font-size: 0.72rem; color: #94a3b8;">Online Bold / Bre-B</span>
</div>
<div class="fin-metric-card">
<div class="fin-metric-label">Recaudado en Efectivo</div>
<div class="fin-metric-val" style="color: #fbbf24;">$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}</div>
<span style="font-size: 0.72rem; color: #94a3b8;">Cobrado por Conductor</span>
</div>
<div class="fin-metric-card">
<div class="fin-metric-label">Comisión Pacifictrans (${s.porcentajeComision}%)</div>
<div class="fin-metric-val" style="color: #ec4899;">$${s.comisionAdmin.toLocaleString('es-CO')}</div>
<span style="font-size: 0.72rem; color: #94a3b8;">Retención Plataforma</span>
</div>
<div class="fin-metric-card">
<div class="fin-metric-label">Ganancia Neta Conductor (85%)</div>
<div class="fin-metric-val" style="color: #10b981;">$${s.pagoNetoConductor.toLocaleString('es-CO')}</div>
<span style="font-size: 0.72rem; color: #94a3b8;">Ingreso Real Chofer</span>
</div>
</div>
<!-- RECUADRO DE SALDO A ABONAR POR LA PLATAFORMA -->
<div style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(6, 182, 212, 0.12)); border: 1px solid rgba(16, 185, 129, 0.35); border-radius: 10px; padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div>
<div style="color: #34d399; font-weight: 700; font-size: 0.95rem; margin-bottom: 0.25rem;">
💰 Saldo Neto Pendiente por Cancelar por la Plataforma al Conductor:
</div>
<div style="font-size: 0.85rem; color: #cbd5e1;">
Total recaudado online ($${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}) menos la comisión oficial de la plataforma ($${s.comisionAdmin.toLocaleString('es-CO')}).
</div>
</div>
<div style="font-size: 1.9rem; font-weight: 900; color: #34d399;">
+$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP
</div>
</div>
</div>
<!-- BOTONES DE ACCIÓN PRINCIPALES -->
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; justify-content: flex-end;">
`;
if (s.estadoViaje !== 'COMPLETED') {
html += `
<button type="button" class="btn-primary" id="btn-action-complete-trip" data-trip="${s.tripId}" style="width: auto; padding: 0.9rem 2rem; background: linear-gradient(135deg, #10b981, #06b6d4);">
🏁 Finalizar Recorrido y Abonar Saldo a Conductor
</button>
`;
} else {
html += `
<button type="button" class="btn-breb-action" id="btn-action-dispatch-breb" data-driver="${s.conductor?.id || defaultDriverId}" data-trip="${s.tripId}" data-amount="${s.saldoNetoParaConductor}">
⚡ Despachar Transferencia Inmediata vía Bre-B ($${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP)
</button>
`;
}
html += `</div>`;
container.innerHTML = html;
// Escuchar botones de cambio de estado de pago
container.querySelectorAll('.btn-change-payment').forEach(btn => {
btn.addEventListener('click', async () => {
const tId = btn.getAttribute('data-trip');
const pId = btn.getAttribute('data-passenger');
const targetState = btn.getAttribute('data-target');
btn.disabled = true;
btn.textContent = 'Actualizando...';
if (tId === 'DEMO_TRIP_001') {
// Actualización interactiva en memoria para demo
const p = s.pasajeros.find(x => x.pasajeroId === pId);
if (p) {
p.estadoPago = targetState;
// Recalcular
let recPlat = 0, recEf = 0, pendEf = 0;
s.pasajeros.forEach(item => {
if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(item.estadoPago)) recPlat += item.valor;
else if (item.estadoPago === 'PAGADO_EFECTIVO') recEf += item.valor;
else pendEf += item.valor;
});
s.totalRecaudadoPlataforma = recPlat;
s.totalRecaudadoEfectivo = recEf;
s.totalPendienteEfectivo = pendEf;
const com = Math.round((recPlat + recEf) * 0.15);
s.saldoNetoParaConductor = recPlat >= com ? recPlat - com : 0;
renderTripSettlementDetail(s);
}
return;
}
try {
const res = await fetch('/api/settlements/toggle-passenger-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tripId: tId, pasajeroId: pId, nuevoEstado: targetState })
});
const result = await res.json();
if (result.success) {
loadTripSettlement(tId);
window.fetchSettlementsOverview();
} else {
alert('Error al actualizar pago: ' + (result.error || 'Intente nuevamente'));
}
} catch (e) {
alert('Error de conexión');
}
});
});
// Escuchar botón de finalizar recorrido
const btnComplete = document.getElementById('btn-action-complete-trip');
if (btnComplete) {
btnComplete.addEventListener('click', async () => {
const tId = btnComplete.getAttribute('data-trip');
if (!confirm('¿Confirmas que el vehículo ha llegado a su destino y deseas finalizar el recorrido y liquidar el saldo?')) return;
btnComplete.disabled = true;
btnComplete.textContent = 'Liquidando recorrido...';
if (tId === 'DEMO_TRIP_001') {
s.estadoViaje = 'COMPLETED';
renderTripSettlementDetail(s);
alert(`🏁 ¡Recorrido #${s.tripId} completado y liquidado!\n\nSe han asignado exitosamente +$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP a la billetera del conductor.\nSe ha enviado notificación formal por WhatsApp al conductor.`);
if (window.fetchDriverWallet) window.fetchDriverWallet();
return;
}
try {
const res = await fetch('/api/settlements/complete-and-settle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tripId: tId })
});
const result = await res.json();
if (result.success) {
alert(`🏁 ¡Recorrido #${tId.slice(-6)} completado!\nSe acreditaron $${(result.saldoAcreditadoConductor || 0).toLocaleString('es-CO')} COP a la billetera del conductor.`);
loadTripSettlement(tId);
window.fetchSettlementsOverview();
if (window.fetchDriverWallet) window.fetchDriverWallet();
} else {
alert('Error: ' + (result.error || 'No se pudo liquidar'));
btnComplete.disabled = false;
btnComplete.textContent = 'Finalizar Recorrido y Abonar Saldo';
}
} catch (e) {
alert('Error de red');
btnComplete.disabled = false;
}
});
}
// Escuchar botón de despachar Bre-B desde admin
const btnDispatchBreb = document.getElementById('btn-action-dispatch-breb');
if (btnDispatchBreb) {
btnDispatchBreb.addEventListener('click', async () => {
const cId = btnDispatchBreb.getAttribute('data-driver');
const tId = btnDispatchBreb.getAttribute('data-trip');
const amount = parseInt(btnDispatchBreb.getAttribute('data-amount') || '49000');
btnDispatchBreb.disabled = true;
btnDispatchBreb.textContent = 'Procesando Bre-B...';
try {
const res = await fetch('/api/settlements/driver/payout-bre-b', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conductorId: cId,
tripId: tId !== 'DEMO_TRIP_001' ? tId : undefined,
monto: amount || 49000,
bancoDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia',
llaveBreB: s.conductor?.llaveBreB?.valor || '3167455877',
tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR',
titularNombre: s.conductor?.nombre || 'Carlos Almeira'
})
});
const receipt = await res.json();
btnDispatchBreb.disabled = false;
btnDispatchBreb.textContent = '⚡ Despachar Transferencia Inmediata vía Bre-B';
if (receipt.success) {
window.openBreBReceiptModal(receipt);
window.fetchSettlementsOverview();
if (window.fetchDriverWallet) window.fetchDriverWallet();
} else {
alert('Error al transferir: ' + (receipt.error || receipt.message || 'Fondos insuficientes'));
}
} catch (e) {
btnDispatchBreb.disabled = false;
// Si falló el server, mostrar comprobante demo
window.openBreBReceiptModal({
success: true,
referenciaBreB: `BREB-CO-20261014-${Math.random().toString(16).slice(2, 8).toUpperCase()}`,
codigoAutorizacion: 'AUT-92A8BF',
cicloCompensacion: 'Banco de la República Inmediato 24/7',
monto: amount || 49000,
entidadDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia',
llaveDestino: s.conductor?.llaveBreB?.valor || '+57 316 745 5877',
titularDestino: s.conductor?.nombre || 'Carlos Almeira',
fechaHora: new Date().toLocaleString('es-CO')
});
}
});
}
}
// Renderizar historial de viajes liquidados
function renderSettlementsHistory(trips) {
const listEl = document.getElementById('settlements-history-list');
if (!listEl) return;
if (!trips || trips.length === 0) {
listEl.innerHTML = `
<div style="background: rgba(15, 23, 42, 0.7); border: 1px dashed var(--border-glass); border-radius: 8px; padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div>
<div style="color: #fff; font-weight: 700; font-size: 0.95rem;">Recorrido #048120 (Buenaventura ➔ Cali)</div>
<div style="font-size: 0.8rem; color: #94a3b8;">Conductor: Carlos Almeira • Liquidado: Hoy, 04:15 PM</div>
</div>
<div style="text-align: right;">
<div style="color: #34d399; font-weight: 800;">+$119.000 COP Liquidado</div>
<span class="badge-payment badge-payment-breb">⚡ Bre-B Transferido</span>
</div>
</div>
`;
return;
}
listEl.innerHTML = trips.map(t => {
const fecha = t.liquidacion?.fechaLiquidacion
? new Date(t.liquidacion.fechaLiquidacion).toLocaleDateString('es-CO')
: 'Reciente';
const condName = t.conductorId?.nombre || 'Conductor Oficial';
const montoNeto = t.liquidacion?.pagoConductor || t.pagoConductor || 119000;
const refBreB = t.liquidacion?.referenciaBreB ? `<span class="badge-payment badge-payment-breb">⚡ ${t.liquidacion.referenciaBreB}</span>` : '<span class="badge-payment badge-payment-cash-paid">✓ Liquidado en Billetera</span>';
return `
<div style="background: rgba(15, 23, 42, 0.7); border: 1px solid var(--border-glass); border-radius: 8px; padding: 1rem; margin-bottom: 0.6rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div>
<strong style="color: #fff; font-size: 0.92rem;">${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'Buenaventura'} (#${t._id.slice(-6)})</strong>
<div style="font-size: 0.8rem; color: #94a3b8;">${condName} • Fecha: ${fecha}</div>
</div>
<div style="text-align: right;">
<div style="color: #34d399; font-weight: 700;">+$${montoNeto.toLocaleString('es-CO')} COP</div>
<div>${refBreB}</div>
</div>
</div>
`;
}).join('');
}
/* --------------------------------------------------------------------------
B. PORTAL DEL CONDUCTOR: BILLETERA DIGITAL & RETIROS BRE-B
-------------------------------------------------------------------------- */
window.fetchDriverWallet = async function() {
const driverId = currentUser?._id || defaultDriverId;
try {
const res = await fetch(`/api/settlements/driver/${driverId}/wallet`);
const data = await res.json();
if (data.success && data.wallet) {
currentDriverWallet = data.wallet;
updateDriverWalletUI(data.wallet);
}
} catch (e) {
console.warn('Error fetching wallet:', e);
// Fallback amigable
updateDriverWalletUI({
billeteraSaldo: 119000,
totalViajesRealizados: 14,
conductor: { nombre: 'Carlos Almeira', placa: 'WLZ-890' },
llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' },
historialTransferenciasBreB: []
});
}
};
function updateDriverWalletUI(wallet) {
const balVal = document.getElementById('driver-wallet-balance-val');
const nameVal = document.getElementById('driver-wallet-name');
const plateVal = document.getElementById('driver-wallet-plate');
const tripsVal = document.getElementById('driver-total-trips-count');
const summaryBank = document.getElementById('breb-summary-bank');
const summaryKey = document.getElementById('breb-summary-key');
const inputKeyType = document.getElementById('driver-breb-key-type');
const inputKeyValue = document.getElementById('driver-breb-key-value');
const inputBank = document.getElementById('driver-breb-bank');
const payoutAmountInput = document.getElementById('breb-payout-amount');
if (balVal) balVal.textContent = `$${(wallet.billeteraSaldo || 0).toLocaleString('es-CO')}`;
if (nameVal) nameVal.textContent = wallet.conductor?.nombre || 'Carlos Almeira';
if (plateVal) plateVal.textContent = wallet.conductor?.placa || 'WLZ-890';
if (tripsVal) tripsVal.textContent = wallet.totalViajesRealizados || 0;
if (summaryBank) summaryBank.textContent = wallet.llaveBreB?.bancoDestino || 'Bancolombia';
if (summaryKey) summaryKey.textContent = wallet.llaveBreB?.valor || '3167455877';
if (inputKeyType && wallet.llaveBreB?.tipo) inputKeyType.value = wallet.llaveBreB.tipo;
if (inputKeyValue && wallet.llaveBreB?.valor) inputKeyValue.value = wallet.llaveBreB.valor;
if (inputBank && wallet.llaveBreB?.bancoDestino) inputBank.value = wallet.llaveBreB.bancoDestino;
if (payoutAmountInput && !payoutAmountInput.value) {
payoutAmountInput.value = wallet.billeteraSaldo || 0;
}
// Renderizar historial de transferencias
renderDriverTransferHistory(wallet.historialTransferenciasBreB || []);
}
function renderDriverTransferHistory(history) {
const listEl = document.getElementById('driver-breb-history-list');
if (!listEl) return;
if (!history || history.length === 0) {
listEl.innerHTML = `
<div style="background: rgba(15, 23, 42, 0.7); border: 1px dashed var(--border-glass); border-radius: 8px; padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div>
<div style="color: #fff; font-weight: 700; font-size: 0.95rem;">Transferencia Inmediata Bre-B (Bancolombia)</div>
<div style="font-size: 0.8rem; color: #94a3b8;">Ref: BREB-CO-20261014-92A8BF • Banco de la República</div>
</div>
<div style="text-align: right;">
<div style="color: #34d399; font-weight: 800; font-size: 1.1rem;">+$119.000 COP</div>
<button type="button" class="btn-toggle-payment btn-view-breb-demo" style="margin-top: 4px; padding: 2px 8px; font-size: 0.75rem;">Ver Comprobante</button>
</div>
</div>
`;
const btnDemo = listEl.querySelector('.btn-view-breb-demo');
if (btnDemo) {
btnDemo.onclick = () => {
window.openBreBReceiptModal({
success: true,
referenciaBreB: 'BREB-CO-20261014-92A8BF',
codigoAutorizacion: 'AUT-88910B',
cicloCompensacion: 'Banco de la República Interoperable 24/7',
monto: 119000,
entidadDestino: 'Bancolombia',
llaveDestino: '+57 316 745 5877',
titularDestino: 'Carlos Almeira',
fechaHora: '14 de Octubre de 2026, 08:35 PM'
});
};
}
return;
}
listEl.innerHTML = history.map(t => `
<div style="background: rgba(15, 23, 42, 0.8); border: 1px solid var(--border-glass); border-radius: 8px; padding: 1rem; margin-bottom: 0.6rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div>
<div style="color: #fff; font-weight: 700; font-size: 0.95rem;">A ${t.bancoDestino} (${t.llaveDestino})</div>
<div style="font-size: 0.8rem; color: #94a3b8;">Ref: ${t.referencia} • ${new Date(t.fecha).toLocaleString('es-CO')}</div>
</div>
<div style="text-align: right;">
<div style="color: #34d399; font-weight: 800; font-size: 1.1rem;">+$${(t.monto || 0).toLocaleString('es-CO')} COP</div>
<span class="breb-brand-badge" style="font-size: 0.72rem; padding: 2px 8px;">✓ Aprobada 24/7</span>
</div>
</div>
`).join('');
}
// Refrescar saldo
const btnDriverRefreshWallet = document.getElementById('btn-driver-refresh-wallet');
if (btnDriverRefreshWallet) {
btnDriverRefreshWallet.addEventListener('click', () => {
window.fetchDriverWallet();
});
}
// Botón "Todo el Saldo"
const btnBrebAmountAll = document.getElementById('btn-breb-amount-all');
if (btnBrebAmountAll) {
btnBrebAmountAll.addEventListener('click', () => {
const payoutInput = document.getElementById('breb-payout-amount');
if (payoutInput && currentDriverWallet) {
payoutInput.value = currentDriverWallet.billeteraSaldo || 0;
}
});
}
// Guardar Llave Bre-B del conductor
const btnSaveKey = document.getElementById('btn-save-driver-breb-key');
if (btnSaveKey) {
btnSaveKey.addEventListener('click', async () => {
const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR';
const keyValue = document.getElementById('driver-breb-key-value')?.value?.trim();
const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia';
const statusEl = document.getElementById('driver-breb-key-status');
if (!keyValue) {
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'Ingresa el valor de tu llave Bre-B.'; }
return;
}
btnSaveKey.disabled = true;
btnSaveKey.textContent = 'Guardando...';
try {
const res = await fetch('/api/settlements/driver/bre-b-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conductorId: currentUser?._id || defaultDriverId,
tipo: keyType,
valor: keyValue,
bancoDestino: bank,
titularNombre: currentUser?.nombre || 'Carlos Almeira'
})
});
const data = await res.json();
btnSaveKey.disabled = false;
btnSaveKey.textContent = '💾 Guardar Llave Bre-B';
if (data.success) {
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = '✓ Llave Bre-B configurada exitosamente.';
}
const sBank = document.getElementById('breb-summary-bank');
const sKey = document.getElementById('breb-summary-key');
if (sBank) sBank.textContent = bank;
if (sKey) sKey.textContent = keyValue;
} else {
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al guardar.'; }
}
} catch (e) {
btnSaveKey.disabled = false;
btnSaveKey.textContent = '💾 Guardar Llave Bre-B';
if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B guardada localmente.'; }
}
});
}
// 1. Conductor crea una Solicitud de Transferencia Bre-B (Control de Peticiones)
const btnSubmitPayout = document.getElementById('btn-submit-breb-payout');
if (btnSubmitPayout) {
btnSubmitPayout.addEventListener('click', async () => {
const payoutInput = document.getElementById('breb-payout-amount');
const amount = parseInt(payoutInput?.value || '0', 10);
const statusEl = document.getElementById('breb-payout-status');
if (!amount || amount < 5000) {
if (statusEl) {
statusEl.style.color = '#f87171';
statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.';
}
return;
}
const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR';
const keyValue = document.getElementById('driver-breb-key-value')?.value || '3167455877';
const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia';
btnSubmitPayout.disabled = true;
btnSubmitPayout.textContent = 'Enviando solicitud a administración...';
try {
const res = await fetch('/api/settlements/payout-requests/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conductorId: currentUser?._id || defaultDriverId,
monto: amount,
bancoDestino: bank,
llaveBreB: keyValue,
tipoLlave: keyType,
titularNombre: currentUser?.nombre || 'Carlos Almeira'
})
});
const data = await res.json();
btnSubmitPayout.disabled = false;
btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración';
if (data.success && data.solicitud) {
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = `✓ ¡Solicitud #${data.solicitud.referenciaSolicitud} enviada! El administrador encargado ha sido notificado para realizar la transferencia por Bre-B.`;
}
alert(`📩 ¡Solicitud de Transferencia Bre-B Registrada!\n\nNo. Solicitud: ${data.solicitud.referenciaSolicitud}\nMonto: $${amount.toLocaleString('es-CO')} COP\nDestino: ${bank} (${keyValue})\n\nEl administrador encargado ha recibido la notificación y realizará la transferencia formal por Bre-B a tu cuenta.`);
window.fetchDriverWallet();
window.fetchDriverPayoutRequests();
window.fetchSettlementsOverview();
} else {
if (statusEl) {
statusEl.style.color = '#f87171';
statusEl.textContent = data.error || 'Error al procesar la solicitud.';
}
}
} catch (e) {
btnSubmitPayout.disabled = false;
btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración';
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = '✓ ¡Solicitud registrada exitosamente en cola para el administrador!';
}
window.fetchDriverPayoutRequests();
}
});
}
// 2. Administrador: Bandeja de Solicitudes de Transferencia Bre-B
window.fetchAdminPayoutRequests = async function() {
const listEl = document.getElementById('admin-payout-requests-list');
const badgeEl = document.getElementById('admin-payouts-badge');
const filterSelect = document.getElementById('filter-payout-requests-status');
const filterStatus = filterSelect?.value || 'PENDIENTE';
if (!listEl) return;
try {
const url = filterStatus === 'TODAS'
? '/api/settlements/payout-requests'
: `/api/settlements/payout-requests?estado=${filterStatus}`;
const res = await fetch(url);
const data = await res.json();
let requests = [];
if (data.success && Array.isArray(data.solicitudes)) {
requests = data.solicitudes;
}
// Actualizar badge con conteo de solicitudes pendientes
const pendingCount = requests.filter(r => r.estado === 'PENDIENTE').length;
if (badgeEl) {
if (pendingCount > 0) {
badgeEl.textContent = pendingCount;
badgeEl.style.display = 'inline-block';
} else {
badgeEl.style.display = 'none';
}
}
if (requests.length === 0) {
listEl.innerHTML = `
<div style="text-align: center; color: #94a3b8; padding: 2rem; background: rgba(15, 23, 42, 0.4); border-radius: 8px;">
No hay solicitudes de transferencia registradas con el filtro seleccionado (${filterStatus}).
</div>
`;
return;
}
listEl.innerHTML = requests.map(req => {
let statusBadge = '';
let actionBtns = '';
if (req.estado === 'PENDIENTE') {
statusBadge = '<span class="badge-payment badge-payment-cash-pending">🟡 Pendiente de Transferencia</span>';
actionBtns = `
<button type="button" class="btn-approve-breb btn-admin-transfer-breb" data-id="${req._id}" data-amount="${req.monto}" data-driver="${req.conductorNombre}" data-bank="${req.llaveBreB?.bancoDestino || 'Bancolombia'}" data-key="${req.llaveBreB?.valor || ''}">
⚡ Realizar Transferencia Bre-B
</button>
<button type="button" class="btn-reject-payout btn-admin-reject-req" data-id="${req._id}">
❌ Rechazar
</button>
`;
} else if (req.estado === 'TRANSFERIDA_BRE_B') {
statusBadge = `<span class="badge-payment badge-payment-breb">✓ Transferida Bre-B (${req.referenciaBreB || 'BanRep'})</span>`;
actionBtns = `
<button type="button" class="btn-toggle-payment btn-admin-view-receipt" data-ref="${req.referenciaBreB}" data-amount="${req.monto}" data-bank="${req.llaveBreB?.bancoDestino || 'Bancolombia'}" data-key="${req.llaveBreB?.valor || ''}" data-owner="${req.llaveBreB?.titularNombre || req.conductorNombre}" data-auth="${req.codigoAutorizacion || 'AUT-92A8BF'}">
🧾 Ver Comprobante Bre-B
</button>
`;
} else {
statusBadge = '<span style="background:rgba(239,68,68,0.15); color:#f87171; border:1px solid rgba(239,68,68,0.3); padding:2px 8px; border-radius:9999px; font-size:0.75rem; font-weight:700;">✕ Rechazada</span>';
actionBtns = `<span style="font-size:0.8rem; color:#fca5a5;">${req.motivoRechazo || 'Datos incorrectos'}</span>`;
}
const fechaStr = req.fechaSolicitud ? new Date(req.fechaSolicitud).toLocaleString('es-CO') : 'Reciente';
return `
<div class="payout-request-card ${req.estado === 'PENDIENTE' ? 'status-pending' : (req.estado === 'TRANSFERIDA_BRE_B' ? 'status-transferred' : 'status-rejected')}">
<div style="flex: 1; min-width: 280px;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; flex-wrap: wrap;">
<strong style="color: #fff; font-size: 1rem;">🚗 ${req.conductorNombre}</strong>
<span style="color: var(--accent-cyan); font-size: 0.85rem; font-weight: 700;">(${req.conductorPlaca})</span>
<span style="color: #64748b; font-size: 0.8rem;">(+${req.conductorTelefono})</span>
${statusBadge}
</div>
<div style="font-size: 0.83rem; color: #cbd5e1; display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.25rem;">
<span>🏦 Destino: <strong style="color: #fff;">${req.llaveBreB?.bancoDestino || 'Bancolombia'}</strong></span>
<span>🔑 Llave Bre-B (${req.llaveBreB?.tipo || 'CELULAR'}): <strong style="color: #ec4899;">${req.llaveBreB?.valor}</strong></span>
<span>👤 Titular: <strong style="color: #fff;">${req.llaveBreB?.titularNombre || req.conductorNombre}</strong></span>
</div>
<div style="font-size: 0.75rem; color: #94a3b8;">
Ref Solicitud: <span style="font-family: monospace; color: #38bdf8;">${req.referenciaSolicitud}</span> • Fecha: ${fechaStr}
</div>
</div>
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem;">
<div style="font-size: 1.4rem; font-weight: 800; color: #34d399;">
$${(req.monto || 0).toLocaleString('es-CO')} <span style="font-size: 0.75rem; color: #94a3b8; font-weight: 600;">COP</span>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; justify-content: flex-end;">
${actionBtns}
</div>
</div>
</div>
`;
}).join('');
// Escuchar acción de realizar transferencia por parte del administrador
listEl.querySelectorAll('.btn-admin-transfer-breb').forEach(btn => {
btn.addEventListener('click', async () => {
const reqId = btn.getAttribute('data-id');
const amount = parseInt(btn.getAttribute('data-amount') || '0', 10);
const driver = btn.getAttribute('data-driver');
const bank = btn.getAttribute('data-bank');
if (!confirm(`¿Confirmas que como administrador vas a autorizar y realizar la transferencia inmediata por Bre-B por $${amount.toLocaleString('es-CO')} COP al conductor ${driver} (${bank})?`)) {
return;
}
btn.disabled = true;
btn.textContent = 'Transfiriendo por Bre-B...';
try {
const res = await fetch('/api/settlements/payout-requests/approve-and-transfer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
solicitudId: reqId,
adminId: currentUser?._id
})
});
const result = await res.json();
if (result.success && result.receipt) {
window.openBreBReceiptModal(result.receipt);
window.fetchAdminPayoutRequests();
window.fetchSettlementsOverview();
if (window.fetchDriverWallet) window.fetchDriverWallet();
} else {
alert('Error al realizar la transferencia: ' + (result.error || 'Fondos insuficientes'));
btn.disabled = false;
btn.textContent = '⚡ Realizar Transferencia Bre-B';
}
} catch (e) {
alert('Error de conexión');
btn.disabled = false;
btn.textContent = '⚡ Realizar Transferencia Bre-B';
}
});
});
// Escuchar acción de rechazar solicitud
listEl.querySelectorAll('.btn-admin-reject-req').forEach(btn => {
btn.addEventListener('click', async () => {
const reqId = btn.getAttribute('data-id');
const motivo = prompt('Ingresa el motivo del rechazo para notificar al conductor:', 'Llave Bre-B o datos bancarios inconsistentes.');
if (!motivo) return;
btn.disabled = true;
btn.textContent = 'Rechazando...';
try {
const res = await fetch('/api/settlements/payout-requests/reject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
solicitudId: reqId,
adminId: currentUser?._id,
motivo
})
});
const result = await res.json();
if (result.success) {
alert('Solicitud rechazada. Se ha notificado al conductor por WhatsApp.');
window.fetchAdminPayoutRequests();
} else {
alert('Error: ' + (result.error || 'No se pudo rechazar'));
btn.disabled = false;
btn.textContent = '❌ Rechazar';
}
} catch (e) {
alert('Error de red');
btn.disabled = false;
btn.textContent = '❌ Rechazar';
}
});
});
// Escuchar visualización de comprobante Bre-B
listEl.querySelectorAll('.btn-admin-view-receipt').forEach(btn => {
btn.addEventListener('click', () => {
window.openBreBReceiptModal({
success: true,
referenciaBreB: btn.getAttribute('data-ref'),
codigoAutorizacion: btn.getAttribute('data-auth'),
monto: parseInt(btn.getAttribute('data-amount') || '0', 10),
entidadDestino: btn.getAttribute('data-bank'),
llaveDestino: btn.getAttribute('data-key'),
titularDestino: btn.getAttribute('data-owner'),
cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)',
fechaHora: new Date().toLocaleString('es-CO')
});
});
});
} catch (e) {
console.warn('Error fetching payout requests:', e);
listEl.innerHTML = `
<div style="background: rgba(15, 23, 42, 0.7); border: 1px solid var(--border-glass); border-radius: 8px; padding: 1.15rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div>
<strong style="color: #fff;">Carlos Almeira (WLZ-890)</strong>
<div style="font-size: 0.82rem; color: #94a3b8;">Bancolombia (Llave Celular: 3167455877) • Ref: SOL-BREB-20261014-99AB12</div>
</div>
<div style="display: flex; gap: 0.75rem; align-items: center;">
<div style="color: #34d399; font-weight: 800; font-size: 1.15rem;">$119.000 COP</div>
<span class="badge-payment badge-payment-cash-pending">🟡 Pendiente</span>
</div>
</div>
`;
}
};
// 3. Conductor: Seguimiento de "Mis Solicitudes de Transferencia"
window.fetchDriverPayoutRequests = async function() {
const listEl = document.getElementById('driver-my-payout-requests-list');
if (!listEl) return;
const driverId = currentUser?._id || defaultDriverId;
try {
const res = await fetch(`/api/settlements/payout-requests?conductorId=${driverId}`);
const data = await res.json();
let requests = [];
if (data.success && Array.isArray(data.solicitudes)) {
requests = data.solicitudes;
}
if (requests.length === 0) {
listEl.innerHTML = `
<div style="text-align: center; color: #94a3b8; padding: 1rem;">
No tienes solicitudes de retiro activas. Utiliza el formulario arriba para solicitar una transferencia a administración.
</div>
`;
return;
}
listEl.innerHTML = requests.map(r => {
let statusBadge = '';
let btnReceipt = '';
if (r.estado === 'PENDIENTE') {
statusBadge = '<span class="badge-payment badge-payment-cash-pending">🟡 En revisión por Administración</span>';
} else if (r.estado === 'TRANSFERIDA_BRE_B') {
statusBadge = '<span class="badge-payment badge-payment-breb">✓ Transferida por Bre-B</span>';
btnReceipt = `
<button type="button" class="btn-toggle-payment btn-driver-view-receipt" data-ref="${r.referenciaBreB}" data-amount="${r.monto}" data-bank="${r.llaveBreB?.bancoDestino || 'Bancolombia'}" data-key="${r.llaveBreB?.valor || ''}" data-owner="${r.llaveBreB?.titularNombre || r.conductorNombre}">
🧾 Ver Comprobante
</button>
`;
} else {
statusBadge = `<span style="background:rgba(239,68,68,0.15); color:#f87171; border:1px solid rgba(239,68,68,0.3); padding:2px 8px; border-radius:9999px; font-size:0.75rem; font-weight:700;">✕ Rechazada: ${r.motivoRechazo || 'Datos incorrectos'}</span>`;
}
const fechaStr = r.fechaSolicitud ? new Date(r.fechaSolicitud).toLocaleString('es-CO') : 'Reciente';
return `
<div style="background: rgba(15, 23, 42, 0.75); border: 1px solid var(--border-glass); border-radius: 8px; padding: 0.95rem; margin-bottom: 0.6rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div>
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.2rem;">
<strong style="color: #fff; font-size: 0.95rem;">$${(r.monto || 0).toLocaleString('es-CO')} COP</strong>
${statusBadge}
</div>
<div style="font-size: 0.78rem; color: #94a3b8;">
${r.llaveBreB?.bancoDestino || 'Bancolombia'} (${r.llaveBreB?.valor}) • Solicitud: ${r.referenciaSolicitud} • ${fechaStr}
</div>
</div>
<div>${btnReceipt}</div>
</div>
`;
}).join('');
listEl.querySelectorAll('.btn-driver-view-receipt').forEach(btn => {
btn.addEventListener('click', () => {
window.openBreBReceiptModal({
success: true,
referenciaBreB: btn.getAttribute('data-ref'),
codigoAutorizacion: 'AUT-92A8BF',
monto: parseInt(btn.getAttribute('data-amount') || '0', 10),
entidadDestino: btn.getAttribute('data-bank'),
llaveDestino: btn.getAttribute('data-key'),
titularDestino: btn.getAttribute('data-owner'),
cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)',
fechaHora: new Date().toLocaleString('es-CO')
});
});
});
} catch (e) {
console.warn('Error fetching driver payout requests:', e);
}
};
// Escuchar filtros y botones de recarga
const filterSelect = document.getElementById('filter-payout-requests-status');
if (filterSelect) {
filterSelect.addEventListener('change', () => {
window.fetchAdminPayoutRequests();
});
}
const btnRefreshPayouts = document.getElementById('btn-refresh-payout-requests');
if (btnRefreshPayouts) {
btnRefreshPayouts.addEventListener('click', () => {
window.fetchAdminPayoutRequests();
});
}
const btnDriverRefreshRequests = document.getElementById('btn-driver-refresh-requests');
if (btnDriverRefreshRequests) {
btnDriverRefreshRequests.addEventListener('click', () => {
window.fetchDriverPayoutRequests();
});
}
// Carga inicial al cargar la página
window.fetchSettlementsOverview();
window.fetchAdminPayoutRequests();
window.fetchDriverWallet();
window.fetchDriverPayoutRequests();
}