4982 lines
209 KiB
JavaScript
4982 lines
209 KiB
JavaScript
// 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';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
|
||
|