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

4982 lines
209 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

// Pacific Trans GO - Interactive Web Platform & Native Android Integration Logic
/* ==========================================================================
PACIFICTRANS GO - NATIVE ANDROID BRIDGE & AUTONOMOUS API INTERCEPTOR
========================================================================== */
const API_REMOTE_HOST = 'https://pacifictrans.almeira.com.co';
const isCapacitorNative = () => {
return (
(typeof window !== 'undefined' && window.Capacitor && (window.Capacitor.isNativePlatform?.() || window.Capacitor.getPlatform?.() === 'android')) ||
(typeof window !== 'undefined' && (window.location.protocol === 'file:' || window.location.origin.includes('localhost') || window.location.origin.includes('capacitor://')))
);
};
// Interceptor global para que toda llamada fetch('/api/...') use el host remoto automáticamente en la App Nativa
if (typeof window !== 'undefined' && window.fetch) {
const originalFetch = window.fetch;
window.fetch = function (resource, init) {
if (typeof resource === 'string' && resource.startsWith('/api/')) {
if (isCapacitorNative()) {
resource = API_REMOTE_HOST + resource;
}
}
return originalFetch.call(this, resource, init);
};
}
let currentTariffs = {
tarifaPasajeroFija: 80000,
precioBasePaquete: 25000,
precioKmPaquete: 600,
factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 },
seguroBasePaquete: 5000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
radioMaxPoolingMetros: 4000,
numeroAtencionWhatsApp: '573167455877',
nombreSoporte: 'Atención Pacific Trans 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', () => {
initNativeBridge();
initPWA();
initTabs();
initAuthSystem();
initNotificationsSystem();
initPassengerCalculator();
initFreightCalculator();
initLeafletRadarMap();
initReviewsAndStats();
initTariffsManager();
initCouponsManager();
initSettlementsAndBreBSystem();
initUserProfileManager();
initAdministratorsManager();
fetchTariffs();
fetchAdminCoupons();
fetchStatsAndReviews();
});
/* ==========================================================================
NATIVE CAPACITOR ANDROID INTEGRATION MODULE
========================================================================== */
function initNativeBridge() {
initNativeSystemBars();
initNativeBackButton();
initNativeGPS();
initNativeHaptics();
initNativeNetworkMonitor();
applyNativeUIPolish();
}
// 1. Barras de Estado & Splash Screen Nativo
async function initNativeSystemBars() {
if (window.Capacitor?.isPluginAvailable?.('StatusBar')) {
try {
const { StatusBar, Style } = window.Capacitor.Plugins;
await StatusBar.setStyle({ style: Style.Dark });
await StatusBar.setBackgroundColor({ color: '#080c14' });
await StatusBar.setOverlaysWebView({ overlay: false });
} catch (e) {
console.warn('[StatusBar Warning]:', e);
}
}
if (window.Capacitor?.isPluginAvailable?.('SplashScreen')) {
try {
const { SplashScreen } = window.Capacitor.Plugins;
setTimeout(() => {
SplashScreen.hide({ fadeOutDuration: 400 });
}, 700);
} catch (e) {
console.warn('[SplashScreen Warning]:', e);
}
}
}
// 2. Control Inteligente del Botón Físico 'Atrás' en Android
function initNativeBackButton() {
if (!window.Capacitor?.isPluginAvailable?.('App')) return;
const { App } = window.Capacitor.Plugins;
let lastBackTime = 0;
App.addListener('backButton', () => {
// 1. Cerrar modales abiertos o dropdown de notificaciones
const activeModal = document.querySelector('.pwa-modal-overlay.active, #notif-dropdown-panel.show');
if (activeModal) {
activeModal.classList.remove('active', 'show');
triggerHaptic('light');
return;
}
// 2. Si se encuentra en el Dashboard Administrativo o Portal de Conductor, regresar a Vista Pública
const viewAdmin = document.getElementById('view-admin-dashboard');
const viewDriver = document.getElementById('view-driver-portal');
if (viewAdmin && viewAdmin.style.display !== 'none') {
if (typeof window.showView === 'function') window.showView('public');
triggerHaptic('light');
return;
}
if (viewDriver && viewDriver.style.display !== 'none') {
if (typeof window.showView === 'function') window.showView('public');
triggerHaptic('light');
return;
}
// 3. Vista principal: Doble pulsación para salir de la App
const now = Date.now();
if (now - lastBackTime < 2200) {
App.exitApp();
} else {
lastBackTime = now;
showNativeToast('Presiona atrás nuevamente para salir');
triggerHaptic('light');
}
});
}
// 3. Geolocalización GPS Nativa de Alta Precisión
function initNativeGPS() {
const btnGps = document.getElementById('btn-native-gps-cali');
if (!btnGps) return;
btnGps.addEventListener('click', async () => {
triggerHaptic('medium');
btnGps.disabled = true;
const originalText = btnGps.innerHTML;
btnGps.innerHTML = '⏳ Obteniendo GPS...';
try {
let coords = null;
if (window.Capacitor?.isPluginAvailable?.('Geolocation')) {
const { Geolocation } = window.Capacitor.Plugins;
const check = await Geolocation.checkPermissions();
if (check.location !== 'granted') {
const req = await Geolocation.requestPermissions();
if (req.location !== 'granted') {
showNativeToast('⚠️ Permiso de ubicación requerido para GPS');
btnGps.disabled = false;
btnGps.innerHTML = originalText;
return;
}
}
const pos = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 12000
});
coords = { lat: pos.coords.latitude, lng: pos.coords.longitude };
} else if (navigator.geolocation) {
coords = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
p => resolve({ lat: p.coords.latitude, lng: p.coords.longitude }),
err => reject(err),
{ enableHighAccuracy: true, timeout: 12000 }
);
});
}
if (coords) {
// Centrar mapa de radar y posicionar marcador del pasajero
if (leafletMap && typeof L !== 'undefined') {
leafletMap.setView([coords.lat, coords.lng], 16);
const userIcon = L.divIcon({
className: 'user-live-gps-pin',
html: '<div class="pulse-user-dot">📍</div>',
iconSize: [36, 36],
iconAnchor: [18, 18]
});
L.marker([coords.lat, coords.lng], { icon: userIcon })
.addTo(leafletMap)
.bindPopup('<b>📍 Tu Ubicación Actual</b><br>Coordenadas GPS de alta precisión detectadas.')
.openPopup();
}
const addrInput = document.getElementById('cali-address');
if (addrInput) {
addrInput.value = `📍 GPS (${coords.lat.toFixed(5)}, ${coords.lng.toFixed(5)})`;
}
showNativeToast('✅ Ubicación GPS detectada con éxito');
triggerHaptic('success');
}
} catch (err) {
console.warn('[GPS Error]:', err);
showNativeToast('No se pudo obtener el GPS actual');
triggerHaptic('warning');
} finally {
btnGps.disabled = false;
btnGps.innerHTML = originalText;
}
});
}
// 4. Feedback Háptico Táctil
function triggerHaptic(type = 'light') {
if (!window.Capacitor?.isPluginAvailable?.('Haptics')) return;
const { Haptics, ImpactStyle, NotificationType } = window.Capacitor.Plugins;
try {
if (type === 'medium') Haptics.impact({ style: ImpactStyle.Medium });
else if (type === 'heavy') Haptics.impact({ style: ImpactStyle.Heavy });
else if (type === 'success') Haptics.notification({ type: NotificationType.Success });
else if (type === 'warning') Haptics.notification({ type: NotificationType.Warning });
else Haptics.impact({ style: ImpactStyle.Light });
} catch (e) { }
}
function initNativeHaptics() {
document.addEventListener('click', (e) => {
const target = e.target.closest('button, .chip-payment, .btn-qty-seat, .service-pill-btn, .rating-star');
if (target) {
triggerHaptic('light');
}
});
}
// 5. Monitor de Red y Detección de Desconexión en Carretera
function initNativeNetworkMonitor() {
const banner = document.getElementById('native-offline-banner');
const updateStatus = (connected) => {
if (!connected) {
if (banner) banner.style.display = 'block';
triggerHaptic('warning');
} else {
if (banner) banner.style.display = 'none';
if (typeof fetchTariffs === 'function') fetchTariffs();
}
};
if (window.Capacitor?.isPluginAvailable?.('Network')) {
const { Network } = window.Capacitor.Plugins;
Network.getStatus().then(status => updateStatus(status.connected));
Network.addListener('networkStatusChange', status => updateStatus(status.connected));
} else {
window.addEventListener('online', () => updateStatus(true));
window.addEventListener('offline', () => updateStatus(false));
if (!navigator.onLine) updateStatus(false);
}
}
// 6. Notificaciones Toast Nativas
function showNativeToast(message, duration = 2400) {
const existing = document.querySelector('.native-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'native-toast';
toast.innerHTML = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// 7. Adaptaciones Visuales en Modo Nativo
function applyNativeUIPolish() {
if (isCapacitorNative()) {
const navApp = document.getElementById('nav-link-app');
if (navApp) navApp.style.display = 'none';
const downloadSection = document.getElementById('descarga-apk');
if (downloadSection) {
downloadSection.innerHTML = `
<div style="text-align: center; width: 100%;">
<span style="color: #10b981; font-weight: 700; text-transform: uppercase; font-size: 0.82rem; letter-spacing: 0.08em;">⚡ MODO NATIVO ANDROID ACTIVO</span>
<h3 style="font-size: 1.8rem; color: #fff; margin-top: 0.25rem; margin-bottom: 0.5rem;">Pacific Trans GO Nativa</h3>
<p style="color: var(--text-muted); max-width: 600px; margin: 0 auto; font-size: 0.95rem;">
Ejecutando en hardware acelerado con soporte GPS satelital de alta precisión, hápticos y respaldo offline local.
</p>
</div>
`;
}
}
}
/* ==========================================================================
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 isAuthorized = currentUser && (currentUser.rol === 'ADMIN' || currentUser.rol === 'OPERADOR' || currentUser.esSuperAdmin);
if (isAuthorized) {
if (adminGate) adminGate.style.display = 'none';
if (adminContent) adminContent.style.display = 'block';
const adminNameEl = document.getElementById('admin-user-name');
if (adminNameEl) {
const roleBadge = currentUser.esSuperAdmin ? '👑 Superadministrador' : (currentUser.rol === 'ADMIN' ? '🛡️ Administrador' : '🎧 Operador');
adminNameEl.textContent = `${currentUser.nombre} (${roleBadge})`;
}
// Aplicar filtrado dinámico de menú según rol (Superadmin/Admin vs Operador)
if (typeof window.applyAdminRoleMenuPermissions === 'function') {
window.applyAdminRoleMenuPermissions(currentUser);
}
// Activar la pestaña administrativa activa (por defecto Radar)
if (typeof window.setAdminSubTab === 'function') {
window.setAdminSubTab(window.activeAdminSubTab || 'radar');
}
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 250);
} 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' || currentUser.esSuperAdmin);
if (isDriver) {
if (driverGate) driverGate.style.display = 'none';
if (driverContent) driverContent.style.display = 'block';
const driverTopbarName = document.getElementById('driver-topbar-name') || document.getElementById('driver-active-name');
if (driverTopbarName) {
const placa = currentUser.vehiculoPlaca ? ` (${currentUser.vehiculoPlaca})` : '';
driverTopbarName.textContent = `${currentUser.nombre}${placa}`;
}
const driverWalletName = document.getElementById('driver-wallet-name');
if (driverWalletName) driverWalletName.textContent = currentUser.nombre;
const driverWalletPlate = document.getElementById('driver-wallet-plate');
if (driverWalletPlate && currentUser.vehiculoPlaca) driverWalletPlate.textContent = currentUser.vehiculoPlaca;
if (typeof window.setDriverSubTab === 'function') {
window.setDriverSubTab(window.activeDriverSubTab || 'billetera');
}
} 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');
}
if (currentUser && typeof window.autofillFormsWithUserData === 'function') {
window.autofillFormsWithUserData(currentUser);
}
};
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-pacifictrans') || 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');
localStorage.removeItem('pt_token_expires_at');
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);
});
});
// ==========================================================================
// BARRA DE MENÚ DE NAVEGACIÓN ADMINISTRATIVA POR ROLES
// ==========================================================================
window.activeAdminSubTab = 'radar';
window.activeDriverSubTab = 'billetera';
window.applyAdminRoleMenuPermissions = function (user) {
if (!user) return;
const isOperador = user.rol === 'OPERADOR';
const btnRadar = document.getElementById('btn-admin-subtab-radar');
const btnLiquidaciones = document.getElementById('btn-admin-subtab-liquidaciones');
const btnTarifas = document.getElementById('btn-admin-subtab-tarifas');
const btnCupones = document.getElementById('btn-admin-subtab-cupones');
const btnAdmins = document.getElementById('btn-admin-subtab-administradores');
const btnFacturacion = document.getElementById('btn-admin-subtab-facturacion');
if (btnRadar) btnRadar.style.display = 'inline-flex';
if (btnLiquidaciones) btnLiquidaciones.style.display = 'inline-flex';
if (isOperador) {
// OPERADOR: Solo radar de despacho y liquidaciones de despacho
if (btnTarifas) btnTarifas.style.display = 'none';
if (btnCupones) btnCupones.style.display = 'none';
if (btnAdmins) btnAdmins.style.display = 'none';
if (btnFacturacion) btnFacturacion.style.display = 'none';
// Si estaba en un subtab no autorizado, cambiar a radar
if (['tarifas', 'cupones', 'administradores', 'facturacion'].includes(window.activeAdminSubTab)) {
window.activeAdminSubTab = 'radar';
}
} else {
// ADMIN o SUPERADMIN: Acceso completo a todas las 6 opciones
if (btnTarifas) btnTarifas.style.display = 'inline-flex';
if (btnCupones) btnCupones.style.display = 'inline-flex';
if (btnAdmins) btnAdmins.style.display = 'inline-flex';
if (btnFacturacion) btnFacturacion.style.display = 'inline-flex';
}
};
window.setAdminSubTab = function (targetViewId) {
window.activeAdminSubTab = targetViewId;
const allViews = [
{ id: 'radar', el: document.getElementById('admin-view-radar') },
{ id: 'liquidaciones', el: document.getElementById('admin-view-liquidaciones') },
{ id: 'tarifas', el: document.getElementById('admin-view-tarifas') },
{ id: 'cupones', el: document.getElementById('admin-view-cupones') },
{ id: 'administradores', el: document.getElementById('admin-view-administradores') },
{ id: 'facturacion', el: document.getElementById('admin-view-facturacion') }
];
allViews.forEach(v => {
if (v.el) {
if (v.id === targetViewId) {
v.style.display = 'block';
v.classList.add('admin-panel-section');
} else {
v.style.display = 'none';
v.classList.remove('admin-panel-section');
}
}
});
// Actualizar indicador y estado activo en la barra de menú administrativa
document.querySelectorAll('#admin-role-nav-menubar .menu-item-btn').forEach(btn => {
const subtab = btn.getAttribute('data-subtab');
if (subtab === targetViewId) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
if (targetViewId === 'radar' && leafletMap) {
setTimeout(() => leafletMap.invalidateSize(), 200);
}
if (targetViewId === 'liquidaciones') {
if (window.fetchSettlementsOverview) window.fetchSettlementsOverview();
if (window.fetchAdminPayoutRequests) window.fetchAdminPayoutRequests();
}
if (targetViewId === 'tarifas' && typeof fetchTariffs === 'function') {
fetchTariffs();
}
if (targetViewId === 'cupones' && window.fetchAdminCoupons) {
window.fetchAdminCoupons();
}
if (targetViewId === 'administradores' && window.fetchAdministrators) {
window.fetchAdministrators();
}
if (targetViewId === 'facturacion' && window.fetchMonthlyBillingReport) {
window.fetchMonthlyBillingReport();
}
};
// Asignar listeners a todos los botones del menú de administración
document.querySelectorAll('#admin-role-nav-menubar .menu-item-btn').forEach(btn => {
btn.addEventListener('click', () => {
const subtab = btn.getAttribute('data-subtab');
if (subtab && typeof window.setAdminSubTab === 'function') {
window.setAdminSubTab(subtab);
}
});
});
// ==========================================================================
// BARRA DE MENÚ OFICIAL DEL CONDUCTOR
// ==========================================================================
window.setDriverSubTab = function (targetViewId) {
window.activeDriverSubTab = targetViewId;
const viewDriverWallet = document.getElementById('driver-view-billetera');
const viewDriverRegistro = document.getElementById('driver-view-registro');
if (viewDriverWallet) {
viewDriverWallet.style.display = (targetViewId === 'billetera') ? 'block' : 'none';
if (targetViewId === 'billetera') viewDriverWallet.classList.add('admin-panel-section');
}
if (viewDriverRegistro) {
viewDriverRegistro.style.display = (targetViewId === 'registro') ? 'block' : 'none';
if (targetViewId === 'registro') viewDriverRegistro.classList.add('admin-panel-section');
}
document.querySelectorAll('#driver-role-nav-menubar .menu-item-btn').forEach(btn => {
const subtab = btn.getAttribute('data-subtab');
if (subtab === targetViewId) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
if (targetViewId === 'billetera') {
if (window.fetchDriverWallet) window.fetchDriverWallet();
if (window.fetchDriverPayoutRequests) window.fetchDriverPayoutRequests();
}
};
document.querySelectorAll('#driver-role-nav-menubar .menu-item-btn').forEach(btn => {
btn.addEventListener('click', () => {
const subtab = btn.getAttribute('data-subtab');
if (subtab && typeof window.setDriverSubTab === 'function') {
window.setDriverSubTab(subtab);
}
});
});
// 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 (PACIFIC TRANS GO CON OTP WHATSAPP + EMAIL)
========================================================================== */
function initAuthSystem() {
const modal = document.getElementById('modal-auth-pacifictrans') || 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 rawPhone = phoneInput?.value.trim() || '';
const email = emailInput?.value.trim() || '';
const name = nameInput?.value.trim() || '';
let cleanPhone = rawPhone.replace(/\D/g, '');
if (cleanPhone.startsWith('57') && cleanPhone.length === 12) {
// Prefijo 57 incluido
} else if (cleanPhone.length === 10) {
cleanPhone = '57' + cleanPhone;
}
if (!cleanPhone || cleanPhone.length < 10) {
showError(reqError, 'Ingresa un número de celular válido (Ej: 300 123 4567).');
return;
}
currentIdentifier = cleanPhone;
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: cleanPhone, 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 || `+${cleanPhone}`;
verError.style.display = 'none';
verError.innerHTML = '';
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) {
const expiresAt = data.expiresAt || (Date.now() + 24 * 60 * 60 * 1000);
localStorage.setItem('pt_token', data.token);
localStorage.setItem('pt_token_expires_at', String(expiresAt));
currentUser = data.user;
updateUserUI(currentUser);
modal.classList.remove('active');
alert(`¡Bienvenido a Pacific Trans GO, ${currentUser.nombre}! Tu identidad ha sido verificada con éxito.`);
} else {
showError(verError, data.error || 'Código incorrecto. Verifica los 6 dígitos.');
}
} catch (err) {
showError(verError, 'Error al validar el código OTP.');
} finally {
btnVerifyOtp.disabled = false;
btnVerifyOtp.innerHTML = '✅ Validar y Acceder';
}
}
// Reenviar OTP
if (btnResend) {
btnResend.addEventListener('click', () => {
formStep1.dispatchEvent(new Event('submit'));
});
}
// Cerrar sesión
if (btnLogout) {
btnLogout.addEventListener('click', () => {
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
currentUser = null;
updateUserUI(null);
window.location.hash = '#reservas';
window.showView('public');
});
}
// Validar sesión persistida (vigencia de 24 horas)
checkPersistedSession();
}
async function checkPersistedSession() {
const token = localStorage.getItem('pt_token');
const expiresAt = localStorage.getItem('pt_token_expires_at');
if (!token) return;
// Comprobar lapso de 24 horas en el navegador
if (expiresAt && Date.now() > Number(expiresAt)) {
console.log('⏰ [Sesión Pacific Trans GO]: El token de 24 horas ha expirado. Requiere autenticación OTP.');
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
currentUser = null;
updateUserUI(null);
return;
}
try {
const res = await fetch(`/api/auth/me?token=${encodeURIComponent(token)}`);
if (res.ok) {
const data = await res.json();
if (data.user) {
currentUser = data.user;
if (data.user.tokenExpiresAt) {
localStorage.setItem('pt_token_expires_at', String(data.user.tokenExpiresAt));
}
updateUserUI(currentUser);
console.log(`✅ [Sesión Autenticada]: ${currentUser.nombre} (+${currentUser.telefono}) activo por 24 horas.`);
} else {
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
}
} else {
localStorage.removeItem('pt_token');
localStorage.removeItem('pt_token_expires_at');
}
} catch (err) {
console.warn('⚠️ [Session Verify Error]:', err);
}
}
// Helper centralizado para rellenar por defecto los formularios con la información del usuario autenticado
function autofillFormsWithUserData(user) {
if (!user) return;
// 1. VIAJES DE PASAJEROS: Dirección principal y datos de Pasajero 1 (Titular)
const caliAddrInput = document.getElementById('cali-address');
if (caliAddrInput && (!caliAddrInput.value || caliAddrInput.value.trim() === '') && user.direccionTexto) {
caliAddrInput.value = user.direccionTexto;
}
if (typeof window.setPassenger1Data === 'function') {
window.setPassenger1Data(user);
} else {
const p1Name = document.getElementById('passenger-1-name');
const p1Doc = document.getElementById('passenger-1-doc');
const p1Phone = document.getElementById('passenger-1-phone');
const p1Addr = document.getElementById('passenger-1-addr');
if (p1Name && (!p1Name.value || p1Name.value.trim() === '' || p1Name.value.startsWith('Usuario '))) {
p1Name.value = user.nombre || '';
}
if (p1Doc && (!p1Doc.value || p1Doc.value.trim() === '') && user.documentoIdentidad) {
p1Doc.value = user.documentoIdentidad;
}
if (p1Phone && (!p1Phone.value || p1Phone.value.trim() === '')) {
p1Phone.value = user.telefono || '';
}
if (p1Addr && (!p1Addr.value || p1Addr.value.trim() === '') && user.direccionTexto) {
p1Addr.value = user.direccionTexto;
}
}
// 2. MENSAJERÍA & ENCOMIENDAS (FLETES): Datos del Remitente
const senderNameInput = document.getElementById('sender-name');
const senderPhoneInput = document.getElementById('sender-phone');
if (senderNameInput && (!senderNameInput.value || senderNameInput.value.trim() === '') && user.nombre) {
senderNameInput.value = user.nombre;
}
if (senderPhoneInput && (!senderPhoneInput.value || senderPhoneInput.value.trim() === '') && user.telefono) {
senderPhoneInput.value = user.telefono;
}
}
window.autofillFormsWithUserData = autofillFormsWithUserData;
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');
if (user) {
if (btnAuthOpen) btnAuthOpen.style.display = 'none';
if (userBadge) userBadge.style.display = 'inline-flex';
if (userNameEl) userNameEl.textContent = (user.nombre || 'Usuario').split(' ')[0];
if (userRoleBadge) {
if (user.esSuperAdmin) {
userRoleBadge.className = 'badge-verified superadmin-badge';
userRoleBadge.innerHTML = '👑 SUPERADMIN';
} else if (user.rol === 'ADMIN') {
userRoleBadge.className = 'badge-verified admin-role-badge admin-role-admin';
userRoleBadge.innerHTML = '🛡️ ADMIN';
} else if (user.rol === 'OPERADOR') {
userRoleBadge.className = 'badge-verified admin-role-badge admin-role-operador';
userRoleBadge.innerHTML = '🎧 OPERADOR';
} else {
userRoleBadge.className = 'badge-verified';
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';
}
// Rellenar por defecto los formularios de viajes y encomiendas con los datos del usuario autenticado
autofillFormsWithUserData(user);
// 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');
}
}
// Actualizar notificaciones para el usuario actual (o limpiar si cerró sesión)
if (typeof window.renderUserNotifications === 'function') {
window.renderUserNotifications();
}
}
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 • Pacific Trans 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 oficiales
const elGroupPacific = document.getElementById('cfg-group-pacifictrans');
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';
if (elGroupPacific) elGroupPacific.value = pacificGroup;
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%20Pacific%20Trans%20GO,%20deseo%20informaci%C3%B3n%20sobre%20un%20servicio`;
// 3. Precios en módulo de reservas de pasajeros
if (window.updatePassengerBookingSummary) {
window.updatePassengerBookingSummary();
} else {
const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`;
const totalPriceEl = document.querySelector('.total-price');
if (totalPriceEl) totalPriceEl.textContent = 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 badgePacific = document.getElementById('badge-pacific-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';
}
alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacific Trans GO ➔ ${data.pacificTrans?.groupName || 'Configurado'} (${data.pacificTrans?.resolvedJid || 'JID Asignado'})`);
} 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 Pacific Trans GO',
grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans 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 Oficial: "${payload.grupoWhatsAppPacificTrans}"`;
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 caliAddressInput = document.getElementById('cali-address');
const buenaStopSelect = document.getElementById('buena-stop');
const bookBtn = document.getElementById('btn-book-pool');
const poolDots = document.querySelectorAll('.seat-dot');
const passengersContainer = document.getElementById('passengers-cards-container');
const badgeCupos = document.getElementById('badge-cupos-selected');
const seatButtons = document.querySelectorAll('.btn-qty-seat');
const paymentCards = document.querySelectorAll('.payment-method-card');
let selectedSeats = 1;
let selectedPaymentMethod = 'BRE_B';
let currentSeatsFilled = 3;
// Cache para persistir los datos ingresados al alternar entre 1..4 cupos
const passengersData = [
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' },
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' },
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' },
{ nombre: '', doc: '', phone: '', sameAddress: true, address: '', notes: '' }
];
function saveCurrentFormData() {
for (let i = 1; i <= selectedSeats; i++) {
const nameInput = document.getElementById(`passenger-${i}-name`);
const docInput = document.getElementById(`passenger-${i}-doc`);
const phoneInput = document.getElementById(`passenger-${i}-phone`);
const notesInput = document.getElementById(`passenger-${i}-notes`);
const sameAddrCheck = document.getElementById(`passenger-${i}-same-addr`);
const addrInput = document.getElementById(`passenger-${i}-addr`);
if (nameInput) passengersData[i - 1].nombre = nameInput.value;
if (docInput) passengersData[i - 1].doc = docInput.value;
if (phoneInput) passengersData[i - 1].phone = phoneInput.value;
if (notesInput) passengersData[i - 1].notes = notesInput.value;
if (sameAddrCheck) passengersData[i - 1].sameAddress = sameAddrCheck.checked;
if (addrInput) passengersData[i - 1].address = addrInput.value;
}
}
// Helper para asignar los datos del Pasajero 1 (Titular) desde el usuario autenticado
window.setPassenger1Data = function (user) {
if (!user) return;
if (passengersData && passengersData[0]) {
if (!passengersData[0].nombre || passengersData[0].nombre.startsWith('Usuario ')) {
passengersData[0].nombre = user.nombre || '';
}
if (!passengersData[0].phone) passengersData[0].phone = user.telefono || '';
if (!passengersData[0].doc && user.documentoIdentidad) passengersData[0].doc = user.documentoIdentidad;
if (!passengersData[0].address && user.direccionTexto) passengersData[0].address = user.direccionTexto;
}
const p1Name = document.getElementById('passenger-1-name');
const p1Doc = document.getElementById('passenger-1-doc');
const p1Phone = document.getElementById('passenger-1-phone');
const p1Addr = document.getElementById('passenger-1-addr');
if (p1Name && (!p1Name.value || p1Name.value.trim() === '' || p1Name.value.startsWith('Usuario '))) {
p1Name.value = user.nombre || '';
}
if (p1Doc && (!p1Doc.value || p1Doc.value.trim() === '') && user.documentoIdentidad) {
p1Doc.value = user.documentoIdentidad;
}
if (p1Phone && (!p1Phone.value || p1Phone.value.trim() === '')) {
p1Phone.value = user.telefono || '';
}
if (p1Addr && (!p1Addr.value || p1Addr.value.trim() === '') && user.direccionTexto) {
p1Addr.value = user.direccionTexto;
}
const caliAddr = document.getElementById('cali-address');
if (caliAddr && (!caliAddr.value || caliAddr.value.trim() === '') && user.direccionTexto) {
caliAddr.value = user.direccionTexto;
}
};
function renderPassengerCards(count) {
if (!passengersContainer) return;
// Si hay usuario logueado, prellenar por defecto los datos del Pasajero 1 (Titular)
if (currentUser) {
if (!passengersData[0].nombre || passengersData[0].nombre.startsWith('Usuario ')) {
passengersData[0].nombre = currentUser.nombre || '';
}
if (!passengersData[0].phone) passengersData[0].phone = currentUser.telefono || '';
if (!passengersData[0].doc && currentUser.documentoIdentidad) passengersData[0].doc = currentUser.documentoIdentidad;
if (!passengersData[0].address && currentUser.direccionTexto) passengersData[0].address = currentUser.direccionTexto;
}
let html = '';
for (let i = 1; i <= count; i++) {
const isTitular = (i === 1);
const data = passengersData[i - 1];
html += `
<div class="passenger-card ${isTitular ? 'titular' : 'companion'}" id="card-passenger-${i}">
<div class="passenger-card-header">
<div class="passenger-card-title">
<span>${isTitular ? '👤' : '👥'}</span>
<span>Pasajero ${i} ${isTitular ? '(Titular de la Reserva)' : `(Acompañante #${i - 1})`}</span>
</div>
<span class="passenger-card-badge ${isTitular ? 'titular' : 'companion'}">
Cupo #${i} • ${isTitular ? 'Titular' : 'Acompañante'}
</span>
</div>
<div class="passenger-grid-2">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="passenger-${i}-name" style="font-size: 0.82rem;">Nombre y Apellidos *</label>
<input type="text" class="form-input" id="passenger-${i}-name" placeholder="Ej: María Fernanda Gómez" value="${data.nombre || ''}" required>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="passenger-${i}-doc" style="font-size: 0.82rem;">Documento (C.C. / Pasaporte) *</label>
<input type="text" class="form-input" id="passenger-${i}-doc" placeholder="Ej: 1144123456" value="${data.doc || ''}" required>
</div>
</div>
<div class="passenger-grid-2" style="margin-top: 0.65rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="passenger-${i}-phone" style="font-size: 0.82rem;">WhatsApp / Móvil (+57) *</label>
<input type="tel" class="form-input" id="passenger-${i}-phone" placeholder="Ej: 300 123 4567" value="${data.phone || ''}" required>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="passenger-${i}-notes" style="font-size: 0.82rem;">Equipaje / Notas Especiales</label>
<input type="text" class="form-input" id="passenger-${i}-notes" placeholder="Ej: Maleta 10kg, morral" value="${data.notes || ''}">
</div>
</div>
${!isTitular ? `
<div style="margin-top: 0.75rem;">
<label class="same-address-toggle" for="passenger-${i}-same-addr">
<input type="checkbox" id="passenger-${i}-same-addr" ${data.sameAddress ? 'checked' : ''}>
<span>📍 Recoger en el mismo domicilio que el Pasajero 1 (Titular)</span>
</label>
<div id="passenger-${i}-addr-group" style="display: ${data.sameAddress ? 'none' : 'block'}; margin-top: 0.4rem;">
<label class="form-label" for="passenger-${i}-addr" style="font-size: 0.82rem;">Dirección Específica de Recogida en Cali</label>
<input type="text" class="form-input" id="passenger-${i}-addr" placeholder="Ej: Carrera 1 #24-10, Barrio San Nicolás" value="${data.address || ''}">
</div>
</div>
` : ''}
</div>
`;
}
passengersContainer.innerHTML = html;
// Conectar eventos a los checkboxes de misma dirección
for (let i = 2; i <= count; i++) {
const check = document.getElementById(`passenger-${i}-same-addr`);
const addrGroup = document.getElementById(`passenger-${i}-addr-group`);
if (check && addrGroup) {
check.addEventListener('change', () => {
addrGroup.style.display = check.checked ? 'none' : 'block';
passengersData[i - 1].sameAddress = check.checked;
});
}
}
}
let appliedCoupon = null;
const poolCouponInput = document.getElementById('pool-coupon-code');
const btnApplyPoolCoupon = document.getElementById('btn-apply-pool-coupon');
const poolCouponFeedback = document.getElementById('pool-coupon-feedback');
if (btnApplyPoolCoupon && poolCouponInput) {
btnApplyPoolCoupon.addEventListener('click', async () => {
const code = poolCouponInput.value.trim().toUpperCase();
if (!code) {
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
poolCouponFeedback.textContent = '⚠️ Ingresa el código del bono o cupón.';
poolCouponFeedback.style.display = 'flex';
}
return;
}
btnApplyPoolCoupon.disabled = true;
btnApplyPoolCoupon.textContent = '...';
try {
const subtotal = selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000);
const res = await fetch('/api/coupons/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ codigo: code, serviceType: 'VIAJES', subtotal })
});
const data = await res.json();
if (data.valid) {
appliedCoupon = data;
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-success';
poolCouponFeedback.textContent = data.message;
poolCouponFeedback.style.display = 'flex';
}
} else {
appliedCoupon = null;
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
poolCouponFeedback.textContent = data.message || 'Cupón inválido';
poolCouponFeedback.style.display = 'flex';
}
}
updateSummary();
} catch (err) {
console.error('Coupon validation error:', err);
if (poolCouponFeedback) {
poolCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
poolCouponFeedback.textContent = 'Error al validar cupón con el servidor.';
poolCouponFeedback.style.display = 'flex';
}
} finally {
btnApplyPoolCoupon.disabled = false;
btnApplyPoolCoupon.textContent = 'Aplicar';
}
});
}
function updateSummary() {
const unitPrice = currentTariffs.tarifaPasajeroFija || 35000;
const subtotal = selectedSeats * unitPrice;
let discountAmount = 0;
if (appliedCoupon && appliedCoupon.valid) {
if (appliedCoupon.coupon.tipoDescuento === 'PORCENTAJE') {
discountAmount = Math.round(subtotal * (appliedCoupon.coupon.valorDescuento / 100));
} else {
discountAmount = Math.min(appliedCoupon.coupon.valorDescuento, subtotal);
}
discountAmount = Math.min(discountAmount, subtotal);
}
const totalAmount = Math.max(0, subtotal - discountAmount);
const formattedUnit = `$${unitPrice.toLocaleString('es-CO')} COP`;
const formattedSubtotal = `$${subtotal.toLocaleString('es-CO')} COP`;
const formattedTotal = `$${totalAmount.toLocaleString('es-CO')} COP`;
const summarySubtotalRow = document.getElementById('summary-subtotal-row');
const summarySubtotalPrice = document.getElementById('summary-subtotal-price');
const summaryDiscountRow = document.getElementById('summary-discount-row');
const summaryDiscountAmount = document.getElementById('summary-discount-amount');
const summaryPriceLabel = document.getElementById('summary-price-label');
const summaryTotalPrice = document.getElementById('summary-total-price');
const summaryCapacity = document.getElementById('summary-capacity-text');
const summarySeatsPill = document.getElementById('summary-seats-pill');
const btnBookText = document.getElementById('btn-book-text');
const badgeCuposSelected = document.getElementById('badge-cupos-selected');
if (badgeCuposSelected) {
badgeCuposSelected.textContent = `${selectedSeats} ${selectedSeats === 1 ? 'Cupo Seleccionado' : 'Cupos Seleccionados'}`;
}
if (summarySeatsPill) {
summarySeatsPill.textContent = `${selectedSeats} ${selectedSeats === 1 ? 'Pasajero (1 cupo)' : `Pasajeros (${selectedSeats} cupos)`}`;
}
if (summaryCapacity) {
summaryCapacity.textContent = selectedSeats === 4
? 'Vehículo Completo Exclusivo (4/4 Cupos)'
: `Máximo 4 Pasajeros (${selectedSeats} ${selectedSeats === 1 ? 'cupo solicitado' : 'cupos solicitados'})`;
}
if (discountAmount > 0) {
if (summarySubtotalRow) {
summarySubtotalRow.style.display = 'flex';
if (summarySubtotalPrice) summarySubtotalPrice.textContent = formattedSubtotal;
}
if (summaryDiscountRow) {
summaryDiscountRow.style.display = 'flex';
const typeText = appliedCoupon.coupon.tipoDescuento === 'PORCENTAJE' ? `${appliedCoupon.coupon.valorDescuento}% OFF` : 'Bono Fijo';
if (summaryDiscountAmount) summaryDiscountAmount.textContent = `- $${discountAmount.toLocaleString('es-CO')} COP (${typeText})`;
}
if (summaryPriceLabel) {
summaryPriceLabel.textContent = `Total con Descuento (${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'}):`;
}
} else {
if (summarySubtotalRow) summarySubtotalRow.style.display = 'none';
if (summaryDiscountRow) summaryDiscountRow.style.display = 'none';
if (summaryPriceLabel) {
summaryPriceLabel.textContent = `Total a Pagar (${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'}):`;
}
}
if (summaryTotalPrice) {
summaryTotalPrice.textContent = formattedTotal;
}
// Actualizar texto del badge informativo según el método de pago
const gatewayBadgeText = document.getElementById('summary-gateway-badge-text');
if (gatewayBadgeText) {
if (selectedPaymentMethod === 'BRE_B') {
gatewayBadgeText.innerHTML = '⚡ Transferencia directa por <strong>QR Bre-B</strong> (Inmediato 24/7 sin comisión)';
} else if (selectedPaymentMethod === 'BOLD') {
gatewayBadgeText.innerHTML = '💳 Pago seguro <strong>en línea</strong> (Tarjetas Débito / Crédito, PSE y Nequi)';
} else if (selectedPaymentMethod === 'NEQUI') {
gatewayBadgeText.innerHTML = '📱 Transferencia directa a <strong>Nequi</strong>';
} else {
gatewayBadgeText.innerHTML = '💵 Pago en <strong>efectivo</strong> directamente al conductor al abordar';
}
}
if (btnBookText) {
const methodText = selectedPaymentMethod === 'BRE_B'
? 'Pagar con QR Bre-B'
: (selectedPaymentMethod === 'BOLD'
? 'Pagar en Línea'
: (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar (Efectivo)'));
btnBookText.textContent = `${methodText} ${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'} (${formattedTotal})`;
}
// Actualizar previsualización en dots del vehículo
updateSeatVisuals(selectedSeats);
}
window.updatePassengerBookingSummary = updateSummary;
// Escuchadores de botones de selección de cupos
seatButtons.forEach((btn) => {
btn.addEventListener('click', () => {
saveCurrentFormData();
seatButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedSeats = parseInt(btn.getAttribute('data-seats') || '1', 10);
renderPassengerCards(selectedSeats);
updateSummary();
});
});
// Selector de método de pago
paymentCards.forEach((card) => {
card.addEventListener('click', () => {
paymentCards.forEach(c => c.classList.remove('active'));
card.classList.add('active');
const radio = card.querySelector('input[type="radio"]');
if (radio) {
radio.checked = true;
selectedPaymentMethod = radio.value;
}
updateSummary();
});
});
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();
}
// Render inicial
renderPassengerCards(selectedSeats);
updateSummary();
// Si el usuario ya está autenticado al cargar el módulo, prellenar sus datos en Pasajero 1 y dirección
if (currentUser) {
window.setPassenger1Data(currentUser);
}
// Envío del Formulario y Reserva en MongoDB con Pooling
if (bookBtn) {
bookBtn.addEventListener('click', async (e) => {
e.preventDefault();
saveCurrentFormData();
// 1. Validar dirección principal de Cali
const caliAddress = caliAddressInput ? caliAddressInput.value.trim() : '';
const isCaliOrigin = originSelect ? originSelect.value === 'CALI' : true;
if (isCaliOrigin && !caliAddress) {
alert('📍 Por favor ingresa la dirección de recogida en Cali (domicilio puerta a puerta).');
caliAddressInput?.focus();
return;
}
// 2. Validar que cada pasajero tenga nombre, documento y teléfono
const validatedPassengers = [];
for (let i = 1; i <= selectedSeats; i++) {
const d = passengersData[i - 1];
if (!d.nombre || d.nombre.trim().length < 3) {
alert(`⚠️ Por favor escribe el nombre completo del Pasajero #${i}.`);
document.getElementById(`passenger-${i}-name`)?.focus();
return;
}
if (!d.doc || d.doc.trim().length < 5) {
alert(`⚠️ Por favor ingresa el documento de identidad del Pasajero #${i} (requerido para la póliza contractual SOAT).`);
document.getElementById(`passenger-${i}-doc`)?.focus();
return;
}
if (!d.phone || d.phone.trim().replace(/\D/g, '').length < 7) {
alert(`⚠️ Por favor ingresa un número de teléfono o WhatsApp válido para el Pasajero #${i}.`);
document.getElementById(`passenger-${i}-phone`)?.focus();
return;
}
const pAddress = (!d.sameAddress && d.address && d.address.trim())
? d.address.trim()
: (caliAddress || 'Punto Oficial Buenaventura');
validatedPassengers.push({
nombre: d.nombre.trim(),
documentoIdentidad: d.doc.trim(),
telefono: d.phone.trim(),
direccionRecogida: pAddress,
coordenadas: [-76.5320, 3.4215],
notas: d.notes ? d.notes.trim() : '',
esTitular: (i === 1)
});
}
// 3. Verificación de seguridad OTP
if (!currentUser) {
const p1Phone = validatedPassengers[0].telefono.replace(/\D/g, '');
const authInput = document.getElementById('auth-identifier');
if (authInput && p1Phone) {
authInput.value = p1Phone;
}
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-pacifictrans') || document.getElementById('modal-auth-dripzone'))?.classList.add('active');
return;
}
// 4. Procesar reserva en el Backend
bookBtn.disabled = true;
const originalBtnHtml = bookBtn.innerHTML;
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> Agrupando ${selectedSeats} Cupo(s)...
`;
try {
const payload = {
usuarioId: currentUser ? currentUser._id : undefined,
sentido: isCaliOrigin ? 'CALI_A_BUENAVENTURA' : 'BUENAVENTURA_A_CALI',
direccionTexto: caliAddress || 'Terminal Buenaventura',
coordenadas: [-76.5320, 3.4215],
puntoFijoBuenaventura: {
nombre: buenaStopSelect ? buenaStopSelect.options[buenaStopSelect.selectedIndex].text : 'Terminal de Transporte de Buenaventura',
coordenadas: [-77.0250, 3.8820]
},
metodoPago: selectedPaymentMethod,
cuposSolicitados: selectedSeats,
pasajeros: validatedPassengers,
codigoCupon: appliedCoupon?.coupon?.codigo || undefined,
email: currentUser?.email || ''
};
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/trips/pool', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
const totalOccupied = data.totalCuposOcupados || data.pasajerosAgrupados || 4;
updateSeatVisuals(totalOccupied);
// Mostrar alerta detallada
const alertBox = document.getElementById('pool-dispatch-alert');
const alertTitle = document.getElementById('alert-title');
const alertMsg = document.getElementById('alert-message');
const alertBreakdown = document.getElementById('alert-passengers-breakdown');
const alertBoldArea = document.getElementById('alert-bold-payment-area');
if (alertTitle) {
alertTitle.textContent = totalOccupied === 4
? '🔥 ¡Vehículo Completo (4/4 Cupos) Listo para Despacho!'
: `✅ ¡${selectedSeats} ${selectedSeats === 1 ? 'Cupo Reservado' : 'Cupos Reservados'} Exitosamente! (${totalOccupied}/4 Ocupados)`;
}
if (alertMsg) {
alertMsg.innerHTML = `
<strong>Viaje #${data.tripId?.slice(-6) || ''}</strong> en ruta <code>${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}</code>.<br>
${data.message}
`;
}
if (alertBreakdown) {
let breakdownHtml = `<div style="background: rgba(15,23,42,0.6); padding: 0.85rem; border-radius: 8px; margin-top: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
<div style="font-weight: 700; color: #38bdf8; margin-bottom: 0.4rem;">👥 Pasajeros Registrados en esta Reserva:</div>`;
validatedPassengers.forEach((p, idx) => {
breakdownHtml += `
<div style="font-size: 0.83rem; margin-bottom: 0.3rem;">
<strong>${idx + 1}. ${p.nombre}</strong> (C.C. ${p.documentoIdentidad}) • Tel: +${p.telefono} • Recogida: ${p.direccionRecogida} ${p.notas ? `• 🧳 ${p.notas}` : ''}
</div>
`;
});
breakdownHtml += `</div>`;
alertBreakdown.innerHTML = breakdownHtml;
}
// Renderizar información o enlace de pago según el método seleccionado
if (alertBoldArea) {
if (payload.metodoPago === 'BRE_B') {
alertBoldArea.style.display = 'block';
const cleanWa = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, '');
alertBoldArea.innerHTML = `
<div style="background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.4); border-radius: 12px; padding: 1.25rem; text-align: center;">
<div style="font-weight: 700; color: #34d399; font-size: 1.05rem; margin-bottom: 0.5rem;">
⚡ Transferencia Inmediata mediante QR Bre-B
</div>
<p style="font-size: 0.88rem; color: #cbd5e1; margin-bottom: 0.75rem;">
Total a transferir: <strong>$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP</strong> por ${selectedSeats} cupo(s).
</p>
<div style="background: rgba(15, 23, 42, 0.75); border: 1px dashed rgba(52, 211, 153, 0.4); border-radius: 8px; padding: 0.75rem; max-width: 440px; margin: 0 auto 0.85rem auto; font-size: 0.85rem; color: #e2e8f0; text-align: left;">
<div>🏛️ <strong>Llave Bre-B Oficial (Celular):</strong> <span style="color: #38bdf8; font-weight: 700;">+${cleanWa}</span></div>
<div style="margin-top: 4px;">📲 <strong>Interoperabilidad:</strong> Puedes transferir desde Bancolombia, Nequi, Daviplata, Dale o cualquier entidad financiera sin costo.</div>
<div style="margin-top: 4px; color: #94a3b8; font-size: 0.78rem;">Referencia de Reserva: #${data.tripId?.slice(-6) || 'PT'}</div>
</div>
<a href="https://wa.me/${cleanWa}?text=${encodeURIComponent(`Hola, confirmo transferencia vía Bre-B por $${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP para la reserva #${data.tripId?.slice(-6) || ''}`)}" target="_blank" class="btn-primary" style="display: inline-flex; align-items: center; justify-content: center; text-decoration: none; padding: 0.75rem 1.5rem; font-weight: 700; background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
📲 Enviar Comprobante Bre-B por WhatsApp
</a>
</div>
`;
} else if (data.boldPayment && data.boldPayment.checkoutUrl) {
alertBoldArea.style.display = 'block';
alertBoldArea.innerHTML = `
<div style="background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.4); border-radius: 12px; padding: 1rem; text-align: center;">
<div style="font-weight: 700; color: #34d399; font-size: 1rem; margin-bottom: 0.5rem;">
💳 Enlace de Pago en Línea Seguro Generado
</div>
<p style="font-size: 0.85rem; color: #cbd5e1; margin-bottom: 0.75rem;">
Total a pagar: <strong>$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP</strong> por ${selectedSeats} cupo(s).
</p>
<a href="${data.boldPayment.checkoutUrl}" target="_blank" class="btn-primary" style="display: inline-flex; align-items: center; justify-content: center; text-decoration: none; padding: 0.75rem 1.5rem; font-weight: 700; background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
👉 Abrir Pago Seguro en Línea (Tarjetas, PSE, Nequi)
</a>
</div>
`;
} else {
alertBoldArea.style.display = 'none';
}
}
if (alertBox) {
alertBox.style.display = 'block';
alertBox.scrollIntoView({ behavior: 'smooth' });
}
if (typeof window.addUserNotification === 'function' && currentUser) {
window.addUserNotification({
id: `trip-${data.tripId || Date.now()}`,
icon: '🚗',
title: `¡Reserva Confirmada! (${selectedSeats} Cupo${selectedSeats > 1 ? 's' : ''})`,
desc: `Viaje #${data.tripId?.slice(-6) || ''} en ruta ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'}. Conductor y ruta asignados.`,
time: 'Hace un momento',
unread: true
}, currentUser);
}
const formattedTotal = `$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP`;
bookBtn.innerHTML = `✅ ${selectedSeats} Cupo(s) Confirmado(s) (${formattedTotal})`;
} else {
alert('❌ No se pudo completar la reserva: ' + (data.message || data.error || 'Intenta de nuevo'));
bookBtn.innerHTML = originalBtnHtml;
bookBtn.disabled = false;
}
} catch (err) {
console.error('Error booking pool:', err);
alert('⚠️ Error de conexión con el servidor de pooling. Verifica tu red.');
bookBtn.innerHTML = originalBtnHtml;
bookBtn.disabled = false;
}
});
}
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 Confirmados`;
}
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 freightSubtotalRow = document.getElementById('freight-subtotal-row');
const freightSubtotalPrice = document.getElementById('freight-subtotal-price');
const freightDiscountRow = document.getElementById('freight-discount-row');
const freightDiscountAmount = document.getElementById('freight-discount-amount');
const freightCouponInput = document.getElementById('freight-coupon-code');
const btnApplyFreightCoupon = document.getElementById('btn-apply-freight-coupon');
const freightCouponFeedback = document.getElementById('freight-coupon-feedback');
const btnBookFreight = document.getElementById('btn-book-freight');
const KM_DIST = 115; // Corredor Cali - Buenaventura
let appliedFreightCoupon = null;
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 subtotalBruto = Math.round(subtotal + seguro);
let discountAmount = 0;
if (appliedFreightCoupon && appliedFreightCoupon.valid) {
if (appliedFreightCoupon.coupon.tipoDescuento === 'PORCENTAJE') {
discountAmount = Math.round(subtotalBruto * (appliedFreightCoupon.coupon.valorDescuento / 100));
} else {
discountAmount = Math.min(appliedFreightCoupon.coupon.valorDescuento, subtotalBruto);
}
discountAmount = Math.min(discountAmount, subtotalBruto);
}
const total = Math.max(0, subtotalBruto - discountAmount);
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 (discountAmount > 0) {
if (freightSubtotalRow) {
freightSubtotalRow.style.display = 'flex';
if (freightSubtotalPrice) freightSubtotalPrice.textContent = `$${subtotalBruto.toLocaleString('es-CO')} COP`;
}
if (freightDiscountRow) {
freightDiscountRow.style.display = 'flex';
const typeText = appliedFreightCoupon.coupon.tipoDescuento === 'PORCENTAJE' ? `${appliedFreightCoupon.coupon.valorDescuento}% OFF` : 'Bono Fijo';
if (freightDiscountAmount) freightDiscountAmount.textContent = `- $${discountAmount.toLocaleString('es-CO')} COP (${typeText})`;
}
} else {
if (freightSubtotalRow) freightSubtotalRow.style.display = 'none';
if (freightDiscountRow) freightDiscountRow.style.display = 'none';
}
if (totalPriceEl) totalPriceEl.textContent = `$${total.toLocaleString('es-CO')} COP`;
};
if (packageTypeSelect) packageTypeSelect.addEventListener('change', window.recalculateFreight);
if (declaredValueInput) declaredValueInput.addEventListener('input', window.recalculateFreight);
if (btnApplyFreightCoupon && freightCouponInput) {
btnApplyFreightCoupon.addEventListener('click', async () => {
const code = freightCouponInput.value.trim().toUpperCase();
if (!code) {
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
freightCouponFeedback.textContent = '⚠️ Ingresa el código del bono o cupón de envío.';
freightCouponFeedback.style.display = 'flex';
}
return;
}
btnApplyFreightCoupon.disabled = true;
btnApplyFreightCoupon.textContent = '...';
try {
const rawFactor = parseFloat(packageTypeSelect?.value || '1.0');
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) * rawFactor;
const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0);
const subtotalBruto = Math.round(subtotal + seguro);
const res = await fetch('/api/coupons/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ codigo: code, serviceType: 'ENCOMIENDAS', subtotal: subtotalBruto })
});
const data = await res.json();
if (data.valid) {
appliedFreightCoupon = data;
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-success';
freightCouponFeedback.textContent = data.message;
freightCouponFeedback.style.display = 'flex';
}
} else {
appliedFreightCoupon = null;
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
freightCouponFeedback.textContent = data.message || 'Cupón no válido para encomiendas';
freightCouponFeedback.style.display = 'flex';
}
}
window.recalculateFreight();
} catch (err) {
console.error('Coupon freight error:', err);
if (freightCouponFeedback) {
freightCouponFeedback.className = 'coupon-feedback-pill coupon-feedback-error';
freightCouponFeedback.textContent = 'Error al verificar cupón.';
freightCouponFeedback.style.display = 'flex';
}
} finally {
btnApplyFreightCoupon.disabled = false;
btnApplyFreightCoupon.textContent = 'Aplicar';
}
});
}
// Despacho / Creación de Encomienda con Pago Seguro y Cupón
if (btnBookFreight) {
btnBookFreight.addEventListener('click', async (e) => {
e.preventDefault();
const senderName = document.getElementById('sender-name')?.value.trim() || currentUser?.nombre || '';
const senderPhone = document.getElementById('sender-phone')?.value.trim() || currentUser?.telefono || '';
const receiverPhone = document.getElementById('receiver-phone')?.value.trim();
const packageDesc = document.getElementById('package-desc')?.value.trim();
if (!senderPhone || senderPhone.replace(/\D/g, '').length < 7) {
alert('⚠️ Por favor ingresa el teléfono/WhatsApp del remitente.');
document.getElementById('sender-phone')?.focus();
return;
}
if (!receiverPhone || receiverPhone.replace(/\D/g, '').length < 7) {
alert('⚠️ Por favor ingresa el teléfono/WhatsApp del destinatario.');
document.getElementById('receiver-phone')?.focus();
return;
}
if (!packageDesc) {
alert('⚠️ Por favor describe el contenido de la encomienda.');
document.getElementById('package-desc')?.focus();
return;
}
btnBookFreight.disabled = true;
const originalText = btnBookFreight.innerHTML;
btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Seguro...';
try {
const payload = {
remitenteId: currentUser ? (currentUser._id || currentUser.id) : undefined,
remitenteNombre: senderName || 'Remitente',
remitenteTelefono: senderPhone,
tipoPaquete: packageTypeSelect ? packageTypeSelect.options[packageTypeSelect.selectedIndex].text : 'Paquete Pequeño',
factor: parseFloat(packageTypeSelect?.value || '1.2'),
valorDeclarado: parseFloat(declaredValueInput?.value || '100000'),
telefonoRemitente: senderPhone,
telefonoDestinatario: receiverPhone,
descripcionContenido: packageDesc,
codigoCupon: appliedFreightCoupon?.coupon?.codigo || undefined
};
const res = await fetch('/api/trips/package', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (data.boldPaymentLink) {
window.open(data.boldPaymentLink, '_blank');
alert(`✅ ¡Guía #${data.guiaSeguimiento || 'PT'} generada!\n\nTarifa Liquidada: $${(data.tarifaFinal || data.tarifaCalculada).toLocaleString('es-CO')} COP\n${data.descuentoCupon ? '🎟️ Descuento aplicado: -$' + data.descuentoCupon.descuento.toLocaleString('es-CO') + ' COP\n' : ''}\nSe ha abierto la pasarela de pago seguro en línea para completar el pago.`);
} else {
alert(`✅ Guía generada correctamente: #${data.guiaSeguimiento}`);
}
if (typeof window.addUserNotification === 'function' && currentUser) {
window.addUserNotification({
id: `encomienda-${data.guiaSeguimiento || Date.now()}`,
icon: '📦',
title: `Guía Encomienda #${data.guiaSeguimiento || 'PT'} Generada`,
desc: `Envío ${payload.sentido === 'CALI_A_BUENAVENTURA' ? 'Cali ➔ Buenaventura' : 'Buenaventura ➔ Cali'} registrado con tarifa $${(data.tarifaFinal || data.tarifaCalculada || 0).toLocaleString('es-CO')} COP. Seguimiento satelital activo.`,
time: 'Hace un momento',
unread: true
}, currentUser);
}
} else {
alert('Error al generar envío: ' + (data.error || 'Verifica los datos'));
}
} catch (err) {
console.error('Error package booking:', err);
alert('Error al conectar con el servidor.');
} finally {
btnBookFreight.disabled = false;
btnBookFreight.innerHTML = originalText;
}
});
}
// Prellenar datos del remitente si el usuario ya está autenticado al cargar el módulo
if (currentUser) {
const senderName = document.getElementById('sender-name');
const senderPhone = document.getElementById('sender-phone');
if (senderName && (!senderName.value || senderName.value.trim() === '')) senderName.value = currentUser.nombre || '';
if (senderPhone && (!senderPhone.value || senderPhone.value.trim() === '')) senderPhone.value = currentUser.telefono || '';
}
}
/* ==========================================================================
9. CENTRO DE NOTIFICACIONES & CONTADOR DE MENSAJES PENDIENTES (FILTRADO POR USUARIO)
========================================================================== */
function getUserNotificationsStorageKey(user) {
if (!user) return null;
const phone = (user.telefono || user.id || user._id || '').toString().replace(/\D/g, '');
return phone ? `pt_notifications_${phone}` : `pt_notifications_user_${user._id || user.id}`;
}
function getInitialUserNotifications(user) {
const firstName = (user.nombre || 'Usuario').split(' ')[0];
const notifs = [
{
id: `welcome-${Date.now()}-1`,
icon: '🚗',
title: `¡Bienvenido a Pacific Trans GO, ${firstName}!`,
desc: `Tu cuenta está activa para reservar viajes puerta a puerta y enviar encomiendas entre Cali y Buenaventura.`,
time: 'Hace un momento',
unread: true
},
{
id: `sec-${Date.now()}-2`,
icon: '🛡️',
title: 'Identidad Verificada 2FA',
desc: `Tu número (+${user.telefono || ''}) está validado con OTP. Tus viajes y pagos están 100% protegidos.`,
time: 'Hace un momento',
unread: true
}
];
if (user.esSuperAdmin || user.rol === 'ADMIN') {
notifs.unshift({
id: `admin-${Date.now()}-0`,
icon: '👑',
title: 'Acceso Administrativo Habilitado',
desc: 'Tienes permisos autorizados para configurar tarifas, bonos de descuento y gestionar administradores.',
time: 'Hace un momento',
unread: true
});
} else if (user.rol === 'CONDUCTOR') {
notifs.unshift({
id: `driver-${Date.now()}-0`,
icon: '💳',
title: 'Billetera Conductor Bre-B Activa',
desc: 'Tu cuenta de chofer está habilitada para recibir transferencias de compensación inmediata 24/7.',
time: 'Hace un momento',
unread: true
});
}
return notifs;
}
function renderUserNotifications() {
const badge = document.getElementById('notif-unread-count');
const headerUnread = document.getElementById('notif-header-unread');
const listEl = document.getElementById('notif-items-list');
const waLink = document.getElementById('notif-wa-link');
if (waLink && currentTariffs.numeroAtencionWhatsApp) {
const clean = currentTariffs.numeroAtencionWhatsApp.replace(/\D/g, '');
waLink.href = `https://wa.me/${clean}?text=Hola%20Central%20de%20Despacho%20Pacific%20Trans%20GO`;
}
// Si no hay usuario autenticado, no mostrar notificaciones de otros usuarios
if (!currentUser) {
if (badge) {
badge.textContent = '0';
badge.style.display = 'none';
}
if (headerUnread) {
headerUnread.textContent = '0 nuevas';
headerUnread.style.display = 'none';
}
if (listEl) {
listEl.innerHTML = `
<div style="padding: 1.75rem 1rem; text-align: center; color: #94a3b8; font-size: 0.88rem;">
<div style="font-size: 1.8rem; margin-bottom: 0.5rem;">🔒</div>
<p style="margin: 0 0 0.75rem 0; color: #cbd5e1; font-weight: 500;">
Inicia sesión con tu celular para ver tus notificaciones y mensajes pendientes.
</p>
<button type="button" class="btn-primary" style="display: inline-block; padding: 0.45rem 1.1rem; font-size: 0.82rem; margin: 0 auto;" onclick="document.getElementById('btn-open-auth-modal')?.click()">
Iniciar Sesión
</button>
</div>
`;
}
return;
}
// Usuario autenticado: cargar únicamente sus notificaciones
const key = getUserNotificationsStorageKey(currentUser);
let notifs = [];
try {
const raw = localStorage.getItem(key);
if (raw) {
notifs = JSON.parse(raw);
} else {
notifs = getInitialUserNotifications(currentUser);
localStorage.setItem(key, JSON.stringify(notifs));
}
} catch (e) {
notifs = getInitialUserNotifications(currentUser);
}
const unreadCount = notifs.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 (notifs.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 = notifs.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 = notifs.find(n => n.id === id);
if (target && target.unread) {
target.unread = false;
localStorage.setItem(key, JSON.stringify(notifs));
renderUserNotifications();
}
});
});
}
}
}
window.renderUserNotifications = renderUserNotifications;
window.addUserNotification = function (notification, targetUser) {
const u = targetUser || currentUser;
if (!u) return;
const key = getUserNotificationsStorageKey(u);
if (!key) return;
let list = [];
try {
const raw = localStorage.getItem(key);
list = raw ? JSON.parse(raw) : getInitialUserNotifications(u);
} catch (e) {
list = [];
}
list.unshift({
id: notification.id || `notif-${Date.now()}`,
icon: notification.icon || '🚗',
title: notification.title,
desc: notification.desc,
time: notification.time || 'Hace un momento',
unread: true
});
if (list.length > 20) list = list.slice(0, 20);
localStorage.setItem(key, JSON.stringify(list));
if (currentUser && (currentUser.telefono === u.telefono || currentUser._id === u._id)) {
renderUserNotifications();
}
};
function initNotificationsSystem() {
const btnBell = document.getElementById('btn-toggle-notifs');
const dropdown = document.getElementById('notif-dropdown-panel');
const btnMarkAll = document.getElementById('btn-mark-all-read');
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', () => {
if (!currentUser) return;
const key = getUserNotificationsStorageKey(currentUser);
if (!key) return;
try {
let notifs = JSON.parse(localStorage.getItem(key) || '[]');
notifs.forEach(n => { n.unread = false; });
localStorage.setItem(key, JSON.stringify(notifs));
renderUserNotifications();
} catch (e) { }
});
}
renderUserNotifications();
}
/* ==========================================================================
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 310 987 6543';
if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Mario Fernando Gómez';
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` +
`_Pacific Trans 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) - Mario Fernando Gómez (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: 'Mario Fernando Gómez',
telefono: '573109876543',
placa: 'WLZ-890',
modelo: 'Renault Duster 2024',
llaveBreB: { tipo: 'CELULAR', valor: '3109876543', bancoDestino: 'Bancolombia', titularNombre: 'Mario Fernando Gómez' }
},
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 || 'Mario Fernando Gómez'} (${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 || '3109876543'}</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 en Línea</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; flex-wrap: wrap;">
<strong style="color: #fff; font-size: 0.95rem;">${idx + 1}. ${p.nombre}</strong>
<span style="color: #64748b; font-size: 0.8rem;">(+${p.telefono})</span>
${p.documentoIdentidad ? `<span style="font-size: 0.75rem; background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 4px; color: #94a3b8;">C.C. ${p.documentoIdentidad}</span>` : ''}
${p.esTitular ? '<span style="font-size: 0.7rem; background: rgba(6,182,212,0.18); color: var(--accent-cyan); padding: 1px 6px; border-radius: 4px; font-weight: 700;">Titular</span>' : ''}
</div>
<div style="font-size: 0.82rem; color: #94a3b8;">
📍 Recogida: ${p.direccionRecogida}
${p.notas ? `<br><span style="color: #fbbf24; font-size: 0.78rem;">🧳 Equipaje: ${p.notas}</span>` : ''}
</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;">En Línea / 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 Pacific Trans GO (${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 || '3109876543',
tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR',
titularNombre: s.conductor?.nombre || 'Mario Fernando Gómez'
})
});
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 310 987 6543',
titularDestino: s.conductor?.nombre || 'Mario Fernando Gómez',
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: Mario Fernando Gómez • 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: 'Mario Fernando Gómez', placa: 'WLZ-890' },
llaveBreB: { tipo: 'CELULAR', valor: '3109876543', bancoDestino: 'Bancolombia', titularNombre: 'Mario Fernando Gómez' },
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 || 'Mario Fernando Gómez';
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 || '3109876543';
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 310 987 6543',
titularDestino: 'Mario Fernando Gómez',
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 || 'Usuario Conductor'
})
});
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 || '3109876543';
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 || 'Usuario Conductor'
})
});
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;">Mario Fernando Gómez (WLZ-890)</strong>
<div style="font-size: 0.82rem; color: #94a3b8;">Bancolombia (Llave Celular: 3109876543) • 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();
}
/* ==========================================================================
14. GESTIÓN ADMINISTRATIVA DE BONOS Y CUPONES DE DESCUENTO
========================================================================== */
function initCouponsManager() {
const form = document.getElementById('form-create-coupon');
const typeSelect = document.getElementById('coupon-type-select');
const labelValue = document.getElementById('label-coupon-value');
const inputValue = document.getElementById('coupon-value-input');
const feedbackEl = document.getElementById('coupon-form-feedback');
const btnRefresh = document.getElementById('btn-refresh-coupons');
// Ajustar etiqueta dinámica según tipo de descuento
if (typeSelect && labelValue && inputValue) {
typeSelect.addEventListener('change', () => {
if (typeSelect.value === 'PORCENTAJE') {
labelValue.textContent = 'Valor (% Descuento) *';
inputValue.placeholder = 'Ej: 15';
inputValue.max = '100';
} else {
labelValue.textContent = 'Monto Fijo de Descuento ($ COP) *';
inputValue.placeholder = 'Ej: 5000';
inputValue.removeAttribute('max');
}
});
}
// Crear cupón
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const codeInput = document.getElementById('coupon-code-input');
const descInput = document.getElementById('coupon-desc-input');
const serviceSelect = document.getElementById('coupon-service-select');
const minSubtotalInput = document.getElementById('coupon-min-subtotal');
const limitInput = document.getElementById('coupon-usage-limit');
const expDateInput = document.getElementById('coupon-exp-date');
const activeCheck = document.getElementById('coupon-active-check');
const submitBtn = document.getElementById('btn-submit-coupon');
const code = (codeInput?.value || '').trim().toUpperCase();
const valor = parseFloat(inputValue?.value || '0');
if (!code) {
alert('⚠️ Por favor ingresa el código del cupón.');
codeInput?.focus();
return;
}
if (isNaN(valor) || valor <= 0) {
alert('⚠️ El valor del descuento debe ser mayor a cero.');
inputValue?.focus();
return;
}
if (typeSelect?.value === 'PORCENTAJE' && valor > 100) {
alert('⚠️ El porcentaje de descuento no puede ser mayor a 100%.');
inputValue?.focus();
return;
}
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Creando cupón en MongoDB...';
const payload = {
codigo: code,
descripcion: descInput?.value.trim() || '',
tipoDescuento: typeSelect?.value || 'PORCENTAJE',
valorDescuento: valor,
aplicaPara: serviceSelect?.value || 'TODOS',
montoMinimoSubtotal: parseFloat(minSubtotalInput?.value || '0') || 0,
limiteUsosTotales: limitInput?.value ? parseInt(limitInput.value, 10) : null,
fechaExpiracion: expDateInput?.value ? expDateInput.value : null,
estaActivo: activeCheck ? activeCheck.checked : true
};
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/coupons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (feedbackEl) {
feedbackEl.className = 'coupon-feedback-pill coupon-feedback-success';
feedbackEl.textContent = `✅ Cupón "${data.coupon.codigo}" creado exitosamente.`;
feedbackEl.style.display = 'flex';
}
form.reset();
if (activeCheck) activeCheck.checked = true;
fetchAdminCoupons();
} else {
if (feedbackEl) {
feedbackEl.className = 'coupon-feedback-pill coupon-feedback-error';
feedbackEl.textContent = `❌ ${data.error || 'Error al crear cupón.'}`;
feedbackEl.style.display = 'flex';
}
}
} catch (err) {
console.error('Error creating coupon:', err);
if (feedbackEl) {
feedbackEl.className = 'coupon-feedback-pill coupon-feedback-error';
feedbackEl.textContent = 'Error de conexión con el servidor.';
feedbackEl.style.display = 'flex';
}
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '➕ Crear y Publicar Cupón de Descuento';
}
});
}
if (btnRefresh) {
btnRefresh.addEventListener('click', fetchAdminCoupons);
}
}
async function fetchAdminCoupons() {
const tableBody = document.getElementById('coupons-table-body');
const counterLabel = document.getElementById('coupons-counter-label');
const kpiTotal = document.getElementById('kpi-total-coupons');
const kpiActive = document.getElementById('kpi-active-coupons');
const kpiRedeemed = document.getElementById('kpi-redeemed-coupons');
try {
const res = await fetch('/api/coupons');
const data = await res.json();
if (res.ok && data.success) {
const coupons = data.coupons || [];
if (counterLabel) counterLabel.textContent = `${coupons.length} Registrados`;
if (kpiTotal) kpiTotal.textContent = coupons.length;
const activeCount = coupons.filter(c => c.estaActivo).length;
if (kpiActive) kpiActive.textContent = activeCount;
const totalRedeemed = coupons.reduce((sum, c) => sum + (c.canjesActuales || 0), 0);
if (kpiRedeemed) kpiRedeemed.textContent = totalRedeemed;
if (!tableBody) return;
if (coupons.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; padding: 2rem; color: #94a3b8;">
No hay cupones creados aún. Usa el formulario de la izquierda para crear el primero.
</td>
</tr>
`;
return;
}
tableBody.innerHTML = coupons.map(c => {
const isPorc = c.tipoDescuento === 'PORCENTAJE';
const descFmt = isPorc ? `${c.valorDescuento}% OFF` : `$${c.valorDescuento.toLocaleString('es-CO')} COP OFF`;
const typeBadge = `<span class="coupon-type-badge ${isPorc ? 'coupon-type-porcentaje' : 'coupon-type-fijo'}">${descFmt}</span>`;
let serviceIcon = '🌟';
let serviceClass = 'coupon-service-todos';
let serviceName = 'Todos';
if (c.aplicaPara === 'VIAJES') {
serviceIcon = '🚗';
serviceClass = 'coupon-service-viajes';
serviceName = 'Viajes';
} else if (c.aplicaPara === 'ENCOMIENDAS') {
serviceIcon = '📦';
serviceClass = 'coupon-service-encomiendas';
serviceName = 'Encomiendas';
}
const serviceBadge = `<span class="coupon-service-pill ${serviceClass}">${serviceIcon} ${serviceName}</span>`;
const usageText = c.limiteUsosTotales ? `${c.canjesActuales || 0} / ${c.limiteUsosTotales}` : `${c.canjesActuales || 0} (Sin límite)`;
const statusBtn = `
<button type="button" class="btn-coupon-toggle ${c.estaActivo ? 'active' : 'inactive'}" onclick="window.toggleCouponStatus('${c._id}', ${!c.estaActivo})">
${c.estaActivo ? '🟢 Activo' : '⚪ Inactivo'}
</button>
`;
const deleteBtn = `
<button type="button" class="btn-coupon-delete" title="Eliminar Cupón" onclick="window.deleteAdminCoupon('${c._id}', '${c.codigo}')">
🗑️
</button>
`;
return `
<tr>
<td>
<div class="coupon-code-badge">${c.codigo}</div>
${c.descripcion ? `<div style="font-size: 0.72rem; color: #94a3b8; margin-top: 2px;">${c.descripcion}</div>` : ''}
</td>
<td>${typeBadge}</td>
<td>${serviceBadge}</td>
<td>
<div>${usageText}</div>
${c.montoMinimoSubtotal > 0 ? `<div style="font-size: 0.7rem; color: #64748b;">Min: $${c.montoMinimoSubtotal.toLocaleString('es-CO')}</div>` : ''}
</td>
<td>${statusBtn}</td>
<td>${deleteBtn}</td>
</tr>
`;
}).join('');
}
} catch (err) {
console.error('Error fetching admin coupons:', err);
}
}
window.fetchAdminCoupons = fetchAdminCoupons;
window.toggleCouponStatus = async function (couponId, nextState) {
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/coupons/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: couponId, estaActivo: nextState })
});
if (res.ok) {
fetchAdminCoupons();
}
} catch (err) {
console.error('Error toggling coupon status:', err);
}
};
window.deleteAdminCoupon = async function (couponId, code) {
if (!confirm(`¿Estás seguro de eliminar el cupón "${code}"? Esta acción no se puede deshacer.`)) {
return;
}
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/coupons/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: couponId })
});
if (res.ok) {
fetchAdminCoupons();
}
} catch (err) {
console.error('Error deleting coupon:', err);
}
};
/* ==========================================================================
10. USER PROFILE MANAGER (INFORMACIÓN PERSONAL - CELULAR INMUTABLE)
========================================================================== */
function initUserProfileManager() {
const modal = document.getElementById('modal-user-profile');
const btnOpenNav = document.getElementById('btn-edit-profile-nav');
const btnOpenUser = document.getElementById('btn-open-user-profile');
const btnClose = document.getElementById('btn-close-profile-modal');
const btnCancel = document.getElementById('btn-cancel-profile');
const form = document.getElementById('form-update-profile');
const feedback = document.getElementById('profile-feedback');
const btnSave = document.getElementById('btn-save-profile');
const inputName = document.getElementById('profile-name');
const inputPhone = document.getElementById('profile-phone');
const inputDoc = document.getElementById('profile-doc');
const inputEmail = document.getElementById('profile-email');
const inputAddress = document.getElementById('profile-address');
const inputCity = document.getElementById('profile-city');
function openProfileModal() {
if (!currentUser) {
const authModal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone');
if (authModal) authModal.classList.add('active');
return;
}
if (inputName) inputName.value = currentUser.nombre || '';
if (inputPhone) inputPhone.value = `+${currentUser.telefono || ''}`;
if (inputDoc) inputDoc.value = currentUser.documentoIdentidad || '';
if (inputEmail) inputEmail.value = currentUser.email || '';
if (inputAddress) inputAddress.value = currentUser.direccionTexto || '';
if (inputCity) inputCity.value = currentUser.ciudad || 'Cali';
if (feedback) feedback.style.display = 'none';
if (modal) modal.classList.add('active');
}
function closeProfileModal() {
if (modal) modal.classList.remove('active');
}
if (btnOpenNav) btnOpenNav.addEventListener('click', openProfileModal);
if (btnOpenUser) btnOpenUser.addEventListener('click', openProfileModal);
if (btnClose) btnClose.addEventListener('click', closeProfileModal);
if (btnCancel) btnCancel.addEventListener('click', closeProfileModal);
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentUser) return;
const token = localStorage.getItem('pt_token') || '';
if (!token) {
alert('Sesión no encontrada. Por favor inicia sesión nuevamente.');
return;
}
if (btnSave) {
btnSave.disabled = true;
btnSave.textContent = '⏳ Guardando...';
}
if (feedback) feedback.style.display = 'none';
try {
const payload = {
nombre: inputName ? inputName.value.trim() : '',
documentoIdentidad: inputDoc ? inputDoc.value.trim() : '',
email: inputEmail ? inputEmail.value.trim() : '',
direccionTexto: inputAddress ? inputAddress.value.trim() : '',
ciudad: inputCity ? inputCity.value.trim() : 'Cali'
};
const res = await fetch('/api/users/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (feedback) {
feedback.innerHTML = `<div style="background: rgba(16, 185, 129, 0.2); border: 1px solid #10b981; color: #34d399; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
✅ ${data.message || 'Información actualizada con éxito'}
</div>`;
feedback.style.display = 'block';
}
if (data.user) {
currentUser = { ...currentUser, ...data.user };
updateUserUI(currentUser);
}
setTimeout(() => {
closeProfileModal();
}, 1200);
} else {
if (feedback) {
feedback.innerHTML = `<div style="background: rgba(239, 68, 68, 0.2); border: 1px solid #ef4444; color: #f87171; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
❌ ${data.error || 'No fue posible actualizar tu información'}
</div>`;
feedback.style.display = 'block';
}
}
} catch (err) {
console.error('Error updating user profile:', err);
if (feedback) {
feedback.innerHTML = `<div style="background: rgba(239, 68, 68, 0.2); border: 1px solid #ef4444; color: #f87171; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
❌ Error de red al comunicar con el servidor
</div>`;
feedback.style.display = 'block';
}
} finally {
if (btnSave) {
btnSave.disabled = false;
btnSave.textContent = '💾 Guardar Información Personal';
}
}
});
}
}
/* ==========================================================================
11. ADMINISTRATORS & ACCESS CONTROL MANAGER (EXCLUSIVO SUPERADMINISTRADOR)
========================================================================== */
function initAdministratorsManager() {
const formCreate = document.getElementById('form-create-admin');
const btnRefresh = document.getElementById('btn-refresh-administrators');
const feedbackCreate = document.getElementById('admin-create-feedback');
const btnSubmitCreate = document.getElementById('btn-submit-new-admin');
// Modal Editar Administrador
const modalEdit = document.getElementById('modal-edit-admin');
const btnCloseEdit = document.getElementById('btn-close-edit-admin-modal');
const btnCancelEdit = document.getElementById('btn-cancel-edit-admin');
const formEdit = document.getElementById('form-edit-admin');
const feedbackEdit = document.getElementById('edit-admin-feedback');
const btnSaveEdit = document.getElementById('btn-save-edit-admin');
const editId = document.getElementById('edit-admin-id');
const editName = document.getElementById('edit-admin-name');
const editPhone = document.getElementById('edit-admin-phone');
const editDoc = document.getElementById('edit-admin-doc');
const editEmail = document.getElementById('edit-admin-email');
const editRole = document.getElementById('edit-admin-role');
if (btnRefresh) {
btnRefresh.addEventListener('click', () => fetchAdministrators());
}
function closeEditModal() {
if (modalEdit) modalEdit.classList.remove('active');
}
if (btnCloseEdit) btnCloseEdit.addEventListener('click', closeEditModal);
if (btnCancelEdit) btnCancelEdit.addEventListener('click', closeEditModal);
// Crear Administrador
if (formCreate) {
formCreate.addEventListener('submit', async (e) => {
e.preventDefault();
const token = localStorage.getItem('pt_token') || '';
if (!token) return;
const nameInput = document.getElementById('new-admin-name');
const phoneInput = document.getElementById('new-admin-phone');
const docInput = document.getElementById('new-admin-doc');
const emailInput = document.getElementById('new-admin-email');
const roleSelect = document.getElementById('new-admin-role');
if (btnSubmitCreate) {
btnSubmitCreate.disabled = true;
btnSubmitCreate.textContent = '⏳ Registrando...';
}
if (feedbackCreate) feedbackCreate.style.display = 'none';
try {
const res = await fetch('/api/admin/administrators', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
nombre: nameInput ? nameInput.value.trim() : '',
telefono: phoneInput ? phoneInput.value.trim() : '',
documentoIdentidad: docInput ? docInput.value.trim() : '',
email: emailInput ? emailInput.value.trim() : '',
rol: roleSelect ? roleSelect.value : 'ADMIN'
})
});
const data = await res.json();
if (res.ok && data.success) {
if (feedbackCreate) {
feedbackCreate.innerHTML = `<div style="background: rgba(16, 185, 129, 0.2); border: 1px solid #10b981; color: #34d399; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
✅ Administrador ${data.administrator.nombre} registrado con éxito.
</div>`;
feedbackCreate.style.display = 'block';
}
formCreate.reset();
fetchAdministrators();
setTimeout(() => {
if (feedbackCreate) feedbackCreate.style.display = 'none';
}, 3500);
} else {
if (feedbackCreate) {
feedbackCreate.innerHTML = `<div style="background: rgba(239, 68, 68, 0.2); border: 1px solid #ef4444; color: #f87171; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
❌ ${data.error || 'No fue posible registrar al administrador'}
</div>`;
feedbackCreate.style.display = 'block';
}
}
} catch (err) {
console.error('Error creating administrator:', err);
} finally {
if (btnSubmitCreate) {
btnSubmitCreate.disabled = false;
btnSubmitCreate.textContent = '➕ Registrar Administrador';
}
}
});
}
// Guardar edición de Administrador
if (formEdit) {
formEdit.addEventListener('submit', async (e) => {
e.preventDefault();
const token = localStorage.getItem('pt_token') || '';
if (!token) return;
const adminId = editId ? editId.value : '';
if (!adminId) return;
if (btnSaveEdit) {
btnSaveEdit.disabled = true;
btnSaveEdit.textContent = '⏳ Guardando...';
}
if (feedbackEdit) feedbackEdit.style.display = 'none';
try {
const payload = {
nombre: editName ? editName.value.trim() : '',
documentoIdentidad: editDoc ? editDoc.value.trim() : '',
email: editEmail ? editEmail.value.trim() : '',
rol: editRole ? editRole.value : 'ADMIN'
};
const res = await fetch(`/api/admin/administrators/${adminId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.success) {
if (feedbackEdit) {
feedbackEdit.innerHTML = `<div style="background: rgba(16, 185, 129, 0.2); border: 1px solid #10b981; color: #34d399; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
✅ Cambios guardados correctamente.
</div>`;
feedbackEdit.style.display = 'block';
}
fetchAdministrators();
setTimeout(() => {
closeEditModal();
}, 1200);
} else {
if (feedbackEdit) {
feedbackEdit.innerHTML = `<div style="background: rgba(239, 68, 68, 0.2); border: 1px solid #ef4444; color: #f87171; padding: 0.75rem; border-radius: 8px; font-size: 0.85rem;">
❌ ${data.error || 'Error al guardar los cambios'}
</div>`;
feedbackEdit.style.display = 'block';
}
}
} catch (err) {
console.error('Error updating admin:', err);
} finally {
if (btnSaveEdit) {
btnSaveEdit.disabled = false;
btnSaveEdit.textContent = '💾 Guardar Cambios';
}
}
});
}
}
async function fetchAdministrators() {
const tbody = document.getElementById('table-administrators-body');
const counterLabel = document.getElementById('admins-counter-label');
const kpiAdmins = document.getElementById('kpi-admins-count');
const kpiOperators = document.getElementById('kpi-operators-count');
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/admin/administrators', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok || !data.success) {
if (tbody) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: #f87171; padding: 1.5rem;">
❌ ${data.error || 'Error al consultar equipo de administradores'}
</td></tr>`;
}
return;
}
const admins = data.administrators || [];
// Actualizar contadores
let adminCount = 0;
let operatorCount = 0;
admins.forEach(a => {
if (a.rol === 'OPERADOR') operatorCount++;
else adminCount++;
});
if (counterLabel) counterLabel.textContent = `${admins.length} miembro(s) registrados`;
if (kpiAdmins) kpiAdmins.textContent = adminCount;
if (kpiOperators) kpiOperators.textContent = operatorCount;
if (!tbody) return;
if (admins.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: #94a3b8; padding: 1.5rem;">
No hay administradores registrados en la plataforma.
</td></tr>`;
return;
}
tbody.innerHTML = admins.map(admin => {
const isSuper = !!admin.esSuperAdmin;
const adminDataEncoded = encodeURIComponent(JSON.stringify(admin));
let roleBadge = '';
if (isSuper) {
roleBadge = `<span class="admin-role-badge admin-role-super">👑 SUPERADMIN</span>`;
} else if (admin.rol === 'ADMIN') {
roleBadge = `<span class="admin-role-badge admin-role-admin">🛡️ ADMIN</span>`;
} else {
roleBadge = `<span class="admin-role-badge admin-role-operador">🎧 OPERADOR</span>`;
}
let statusHtml = '';
if (isSuper) {
statusHtml = `<span style="color: #34d399; font-weight: 700; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 4px;">
🔒 Permanente
</span>`;
} else {
const isActive = admin.isActive !== false;
statusHtml = `
<button type="button"
onclick="window.toggleAdminActive('${admin.id}', ${!isActive}, '${admin.nombre}')"
class="btn-toggle-payment ${isActive ? 'active' : 'inactive'}"
title="${isActive ? 'Clic para inactivar administrador' : 'Clic para activar administrador'}"
>
${isActive ? '✅ Activo' : '⏸️ Inactivo'}
</button>
`;
}
let actionsHtml = '';
if (isSuper) {
actionsHtml = `<button type="button" class="btn-admin-immutable" disabled title="El Superadministrador es la máxima autoridad permanente e intocable">🔒 Superadmin</button>`;
} else {
actionsHtml = `
<div style="display: flex; gap: 0.35rem; align-items: center;">
<button type="button" class="btn-admin-edit" onclick="window.openEditAdminModal('${adminDataEncoded}')" title="Editar Administrador">
✏️ Editar
</button>
<button type="button" class="btn-coupon-delete" onclick="window.deleteAdmin('${admin.id}', '${admin.nombre}')" title="Eliminar Administrador">
🗑️
</button>
</div>
`;
}
return `
<tr>
<td>
<div style="font-weight: 700; color: #fff; display: flex; align-items: center; gap: 0.4rem;">
${isSuper ? '<span style="font-size: 1.1rem;">👑</span>' : '<span>🛡️</span>'}
<span>${admin.nombre}</span>
${isSuper ? '<span class="superadmin-badge" style="font-size: 0.65rem; padding: 1px 6px;">👑 PRINCIPAL</span>' : ''}
</div>
<div style="font-size: 0.75rem; color: #94a3b8;">
${admin.documentoIdentidad ? `C.C. ${admin.documentoIdentidad} • ` : ''}${admin.email || 'Sin correo registrado'}
</div>
</td>
<td>
<div style="color: #38bdf8; font-weight: 600; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;">
<span>📲</span>
<span>+${admin.telefono}</span>
</div>
<div style="font-size: 0.7rem; color: #10b981;">2FA OTP Habilitado</div>
</td>
<td>${roleBadge}</td>
<td>${statusHtml}</td>
<td>${actionsHtml}</td>
</tr>
`;
}).join('');
} catch (err) {
console.error('Error fetching administrators:', err);
if (tbody) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: #f87171; padding: 1.5rem;">
❌ Error de conexión al cargar equipo de administradores
</td></tr>`;
}
}
}
window.fetchAdministrators = fetchAdministrators;
window.openEditAdminModal = function (adminDataEncoded) {
try {
const admin = JSON.parse(decodeURIComponent(adminDataEncoded));
const modal = document.getElementById('modal-edit-admin');
const editId = document.getElementById('edit-admin-id');
const editName = document.getElementById('edit-admin-name');
const editPhone = document.getElementById('edit-admin-phone');
const editDoc = document.getElementById('edit-admin-doc');
const editEmail = document.getElementById('edit-admin-email');
const editRole = document.getElementById('edit-admin-role');
const editRoleGroup = document.getElementById('edit-admin-role-group');
const subtitle = document.getElementById('edit-admin-subtitle');
const feedback = document.getElementById('edit-admin-feedback');
if (editId) editId.value = admin.id || '';
if (editName) editName.value = admin.nombre || '';
if (editPhone) editPhone.value = `+${admin.telefono || ''}`;
if (editDoc) editDoc.value = admin.documentoIdentidad || '';
if (editEmail) editEmail.value = admin.email || '';
if (editRole) editRole.value = admin.rol || 'ADMIN';
if (admin.esSuperAdmin) {
if (editRoleGroup) editRoleGroup.style.display = 'none';
if (subtitle) subtitle.textContent = 'Actualizar datos de contacto del Superadministrador permanente';
} else {
if (editRoleGroup) editRoleGroup.style.display = 'block';
if (subtitle) subtitle.textContent = 'Actualizar permisos y datos de contacto del colaborador';
}
if (feedback) feedback.style.display = 'none';
if (modal) modal.classList.add('active');
} catch (e) {
console.error('Error parsing admin for edit:', e);
}
};
window.toggleAdminActive = async function (adminId, nextState, adminName) {
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch(`/api/admin/administrators/${adminId}/toggle-active`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: adminId, isActive: nextState })
});
const data = await res.json();
if (res.ok && data.success) {
fetchAdministrators();
} else {
alert(`Error al cambiar estado: ${data.error || 'No permitido'}`);
}
} catch (err) {
console.error('Error toggling admin active:', err);
alert('Error al comunicar con el servidor.');
}
};
window.deleteAdmin = async function (adminId, adminName) {
if (!confirm(`¿Estás seguro de eliminar a ${adminName || 'este administrador'} del equipo? Esta acción revoca inmediatamente sus permisos de acceso.`)) {
return;
}
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch(`/api/admin/administrators/${adminId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: adminId })
});
const data = await res.json();
if (res.ok && data.success) {
fetchAdministrators();
} else {
alert(`Error al eliminar administrador: ${data.error || 'No permitido'}`);
}
} catch (err) {
console.error('Error deleting administrator:', err);
alert('Error al comunicar con el servidor.');
}
};
// ==============================================================================
// GESTIÓN DE CIERRES MENSUALES & FACTURACIÓN DIAN (PACIFIC TRANS GO)
// ==============================================================================
let currentBillingCierres = [];
let selectedBillingIds = new Set();
window.fetchMonthlyBillingReport = async function (customMonth, customYear) {
const monthSelect = document.getElementById('select-billing-month');
const yearSelect = document.getElementById('select-billing-year');
const tbody = document.getElementById('table-billing-cierres-body');
const btnMark = document.getElementById('btn-billing-mark-invoiced');
const chkAll = document.getElementById('chk-billing-select-all');
const mes = customMonth || (monthSelect ? monthSelect.value : (new Date().getMonth() + 1));
const anio = customYear || (yearSelect ? yearSelect.value : new Date().getFullYear());
if (monthSelect) monthSelect.value = mes;
if (yearSelect) yearSelect.value = anio;
if (tbody) {
tbody.innerHTML = `<tr><td colspan="9" style="text-align: center; padding: 2rem; color: #94a3b8;">Cargando cierres fiscales del período ${mes}/${anio}...</td></tr>`;
}
selectedBillingIds.clear();
if (chkAll) chkAll.checked = false;
if (btnMark) {
btnMark.disabled = true;
btnMark.textContent = '✅ Marcar Seleccionados como Facturado Manual';
}
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch(`/api/admin/billing/reporte-mensual?mes=${mes}&anio=${anio}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await res.json();
if (!data.success) {
if (tbody) tbody.innerHTML = `<tr><td colspan="9" style="text-align: center; padding: 2rem; color: #ef4444;">Error al cargar reporte: ${data.error || 'Desconocido'}</td></tr>`;
return;
}
currentBillingCierres = data.cierres || [];
renderMonthlyBillingTable(currentBillingCierres, mes, anio);
} catch (err) {
console.error('Error fetching monthly billing report:', err);
if (tbody) tbody.innerHTML = `<tr><td colspan="9" style="text-align: center; padding: 2rem; color: #ef4444;">Error de conexión con el servidor.</td></tr>`;
}
};
function renderMonthlyBillingTable(cierres, mes, anio) {
const tbody = document.getElementById('table-billing-cierres-body');
const badge = document.getElementById('billing-results-badge');
const kpiConductores = document.getElementById('kpi-billing-conductores');
const kpiComisiones = document.getElementById('kpi-billing-total-comisiones');
const kpiViajes = document.getElementById('kpi-billing-total-viajes');
const kpiPendientes = document.getElementById('kpi-billing-pendientes');
const kpiFacturadosLabel = document.getElementById('kpi-billing-facturados-label');
if (badge) badge.textContent = `${cierres.length} registros`;
let totalComisiones = 0;
let totalViajes = 0;
let pendientesCount = 0;
let facturadosCount = 0;
cierres.forEach(c => {
totalComisiones += Number(c.total_comisiones_acumuladas || 0);
totalViajes += Number(c.total_viajes_realizados || 0);
if (c.estado_facturacion === 'facturado_manual') {
facturadosCount++;
} else {
pendientesCount++;
}
});
if (kpiConductores) kpiConductores.textContent = cierres.length;
if (kpiComisiones) kpiComisiones.textContent = `$${totalComisiones.toLocaleString('es-CO')}`;
if (kpiViajes) kpiViajes.textContent = totalViajes;
if (kpiPendientes) kpiPendientes.textContent = pendientesCount;
if (kpiFacturadosLabel) kpiFacturadosLabel.textContent = `${facturadosCount} facturados manual (DIAN)`;
if (!tbody) return;
if (cierres.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="9" style="text-align: center; padding: 2.5rem; color: #94a3b8;">
No se encontraron cierres para ${mes}/${anio}. Haz clic en <strong>⚡ Ejecutar Corte Mensual Ahora</strong> para consolidar las comisiones de este mes.
</td>
</tr>
`;
return;
}
tbody.innerHTML = cierres.map(c => {
const id = c.id || c._id;
const isFacturado = c.estado_facturacion === 'facturado_manual';
const isApi = c.estado_facturacion === 'enviado_api';
let estadoBadge = `<span class="badge-status-pending" style="background: rgba(245, 158, 11, 0.18); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.35); padding: 2px 8px; border-radius: 6px; font-size: 0.75rem; font-weight: 600;">⏳ Pendiente Proceso</span>`;
if (isFacturado) {
estadoBadge = `<span class="badge-status-completed" style="background: rgba(16, 185, 129, 0.18); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.35); padding: 2px 8px; border-radius: 6px; font-size: 0.75rem; font-weight: 600;">✅ Facturado Manual (DIAN)</span>`;
} else if (isApi) {
estadoBadge = `<span class="badge-status-completed" style="background: rgba(56, 189, 248, 0.18); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.35); padding: 2px 8px; border-radius: 6px; font-size: 0.75rem; font-weight: 600;">🌐 Enviado API</span>`;
}
const fechaCierreFmt = c.fecha_cierre ? new Date(c.fecha_cierre).toLocaleDateString('es-CO') : '-';
const checked = selectedBillingIds.has(id) ? 'checked' : '';
return `
<tr data-cierre-id="${id}" style="${isFacturado ? 'opacity: 0.85;' : ''}">
<td style="text-align: center;">
<input type="checkbox" class="chk-billing-item" value="${id}" ${checked}>
</td>
<td style="font-family: monospace; font-size: 0.78rem; color: #94a3b8;" title="${id}">
${id.slice(0, 8)}...
</td>
<td style="font-weight: 600; color: #f1f5f9;">
${c.nit_o_cedula || 'NO-REGISTRADO'}
</td>
<td>
<strong style="color: #fff;">${c.razon_social || 'Conductor Pacific Trans'}</strong>
</td>
<td style="color: #cbd5e1; font-size: 0.82rem;">
${c.correo_electronico || 'info@pacifictrans.com'}
</td>
<td style="color: #38bdf8; font-weight: 700; font-size: 0.95rem;">
$${Number(c.total_comisiones_acumuladas || 0).toLocaleString('es-CO')}
</td>
<td style="text-align: center; font-weight: 600; color: #fbbf24;">
${c.total_viajes_realizados || 0}
</td>
<td>
${estadoBadge}
</td>
<td style="font-size: 0.8rem; color: #94a3b8;">
${fechaCierreFmt}
</td>
</tr>
`;
}).join('');
attachBillingItemCheckboxListeners();
}
function attachBillingItemCheckboxListeners() {
const itemCheckboxes = document.querySelectorAll('.chk-billing-item');
const btnMark = document.getElementById('btn-billing-mark-invoiced');
const chkAll = document.getElementById('chk-billing-select-all');
itemCheckboxes.forEach(chk => {
chk.addEventListener('change', () => {
if (chk.checked) {
selectedBillingIds.add(chk.value);
} else {
selectedBillingIds.delete(chk.value);
}
if (btnMark) {
btnMark.disabled = selectedBillingIds.size === 0;
btnMark.textContent = selectedBillingIds.size > 0
? `✅ Marcar (${selectedBillingIds.size}) como Facturado Manual`
: '✅ Marcar Seleccionados como Facturado Manual';
}
if (chkAll) {
chkAll.checked = itemCheckboxes.length > 0 && selectedBillingIds.size === itemCheckboxes.length;
}
});
});
}
// Inicialización de Eventos de Facturación DIAN
document.addEventListener('DOMContentLoaded', () => {
const btnFilter = document.getElementById('btn-billing-filter');
const btnDownloadCsv = document.getElementById('btn-billing-download-csv');
const btnRunCut = document.getElementById('btn-billing-run-cut');
const chkAll = document.getElementById('chk-billing-select-all');
const btnMarkInvoiced = document.getElementById('btn-billing-mark-invoiced');
const monthSelect = document.getElementById('select-billing-month');
const yearSelect = document.getElementById('select-billing-year');
if (btnFilter) {
btnFilter.addEventListener('click', () => {
window.fetchMonthlyBillingReport();
});
}
if (monthSelect) {
monthSelect.addEventListener('change', () => window.fetchMonthlyBillingReport());
}
if (yearSelect) {
yearSelect.addEventListener('change', () => window.fetchMonthlyBillingReport());
}
// Descarga directa del archivo CSV con columnas requeridas por DIAN
if (btnDownloadCsv) {
btnDownloadCsv.addEventListener('click', () => {
const mes = monthSelect ? monthSelect.value : (new Date().getMonth() + 1);
const anio = yearSelect ? yearSelect.value : new Date().getFullYear();
const downloadUrl = `/api/admin/billing/reporte-mensual?mes=${mes}&anio=${anio}&format=csv`;
window.open(downloadUrl, '_blank');
});
}
// Ejecución del corte mensual bajo demanda
if (btnRunCut) {
btnRunCut.addEventListener('click', async () => {
const mes = monthSelect ? monthSelect.value : (new Date().getMonth() + 1);
const anio = yearSelect ? yearSelect.value : new Date().getFullYear();
if (!confirm(`¿Deseas consolidar y generar el corte mensual de comisiones para ${mes}/${anio}?`)) {
return;
}
btnRunCut.disabled = true;
btnRunCut.textContent = '⏳ Ejecutando corte...';
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/admin/billing/ejecutar-cierre', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ mes: parseInt(mes, 10), anio: parseInt(anio, 10) })
});
const data = await res.json();
if (res.ok && data.success) {
alert(`✅ Corte mensual completado exitosamente para ${mes}/${anio}.\nTotal Conductores: ${data.totalConductores}\nTotal Comisiones de Software: $${Number(data.totalComisiones).toLocaleString('es-CO')} COP`);
window.fetchMonthlyBillingReport(mes, anio);
} else {
alert(`Error al ejecutar corte: ${data.error || 'Fallo interno'}`);
}
} catch (err) {
console.error('Error executing billing cut:', err);
alert('Error de comunicación con el servidor.');
} finally {
btnRunCut.disabled = false;
btnRunCut.textContent = '⚡ Ejecutar Corte Mensual Ahora';
}
});
}
// Checkbox Seleccionar Todos
if (chkAll) {
chkAll.addEventListener('change', () => {
const itemCheckboxes = document.querySelectorAll('.chk-billing-item');
itemCheckboxes.forEach(chk => {
chk.checked = chkAll.checked;
if (chkAll.checked) {
selectedBillingIds.add(chk.value);
} else {
selectedBillingIds.delete(chk.value);
}
});
if (btnMarkInvoiced) {
btnMarkInvoiced.disabled = selectedBillingIds.size === 0;
btnMarkInvoiced.textContent = selectedBillingIds.size > 0
? `✅ Marcar (${selectedBillingIds.size}) como Facturado Manual`
: '✅ Marcar Seleccionados como Facturado Manual';
}
});
}
// Marcar seleccionados como facturados manualmente
if (btnMarkInvoiced) {
btnMarkInvoiced.addEventListener('click', async () => {
if (selectedBillingIds.size === 0) return;
const count = selectedBillingIds.size;
if (!confirm(`¿Confirmas marcar ${count} cierres seleccionados como 'Facturado Manual' en la DIAN? Esto evitará duplicidades fiscales.`)) {
return;
}
btnMarkInvoiced.disabled = true;
btnMarkInvoiced.textContent = '⏳ Actualizando...';
try {
const token = localStorage.getItem('pt_token') || '';
const res = await fetch('/api/admin/billing/marcar-facturado', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ IDs_Cierre: Array.from(selectedBillingIds) })
});
const data = await res.json();
if (res.ok && data.success) {
alert(`✅ ${data.updatedCount} cierres actualizados a estado 'facturado_manual'.`);
selectedBillingIds.clear();
window.fetchMonthlyBillingReport();
} else {
alert(`Error al actualizar estado: ${data.error || 'No permitido'}`);
}
} catch (err) {
console.error('Error marking closures as invoiced:', err);
alert('Error de conexión con el servidor.');
} finally {
if (btnMarkInvoiced) {
btnMarkInvoiced.disabled = selectedBillingIds.size === 0;
btnMarkInvoiced.textContent = '✅ Marcar Seleccionados como Facturado Manual';
}
}
});
}
});