/* Autobooking Driver Dashboard v2 — Complete JS */ (function () { 'use strict'; /* ── Bootstrap ──────────────────────────────────────────────── */ const CFG = window.AB_DRIVER_CFG || {}; const ROOT = (CFG.api_root || '').replace(/\/$/, ''); const NONCE = CFG.nonce || ''; const GMAPS_KEY = CFG.gmaps_key || ''; const DRV = CFG.driver || {}; /* ── State ──────────────────────────────────────────────────── */ const S = { online: false, trip: null, tripStatus: null, incidentId: null, mediaRec: null, mediaStream: null, map: null, driverMarker: null, routePolyline: null, suggestPolyline: null, destMarker: null, watchId: null, lat: null, lng: null, accuracy: null, chatLastId: 0, unreadChat: 0, chatOpen: true, earnPeriod: 'day', histPage: 1, histFrom: '', histTo: '', zoneTimers: {}, activeZoneId: null, courtesyInterval: null, courtesySecondsLeft: 180, telemetryInterval: null, tripPollInterval: null, chatPollInterval: null, zonePollInterval: null, sosChunkIndex: 0, sosInterval: null, historyData: [], yearData: null, }; /* ── DOM refs ───────────────────────────────────────────────── */ function el(id) { return document.getElementById(id); } /* ── API helper ─────────────────────────────────────────────── */ async function api(path, method, body) { method = method || 'GET'; const opts = { method, headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE, }, }; if (body) opts.body = JSON.stringify(body); const r = await fetch(ROOT + '/wp-json/autobooking/v1' + path, opts); if (!r.ok) throw new Error(await r.text()); return r.json(); } /* ── Toast ──────────────────────────────────────────────────── */ let toastTimer; function toast(msg, type) { type = type || 'info'; let t = el('abd-toast'); if (!t) { t = document.createElement('div'); t.id = 'abd-toast'; document.body.appendChild(t); } t.textContent = msg; t.className = 'abd-toast--show abd-toast--' + type; clearTimeout(toastTimer); toastTimer = setTimeout(() => { t.className = ''; }, 3000); } /* ── Tab navigation ─────────────────────────────────────────── */ function initTabs() { document.querySelectorAll('.abd-tab').forEach(btn => { btn.addEventListener('click', function () { const tab = this.dataset.tab; document.querySelectorAll('.abd-tab').forEach(b => b.classList.remove('abd-tab--active')); document.querySelectorAll('.abd-panel').forEach(p => { p.style.display = 'none'; p.classList.remove('abd-panel--active'); }); this.classList.add('abd-tab--active'); const panel = el('abd-tab-' + tab); if (panel) { panel.style.display = ''; panel.classList.add('abd-panel--active'); } if (tab === 'ganancias') loadEarnings(S.earnPeriod, true); if (tab === 'metricas') loadMetrics(); if (tab === 'historial') loadHistory(1, S.histFrom, S.histTo); if (tab === 'perfil') loadProfile(); }); }); } /* ── Header / Driver info ────────────────────────────────────── */ function populateHeader() { const nameEl = el('abd-header-name'); const plateEl = el('abd-header-plate'); const photoEl = el('abd-header-photo'); if (nameEl) nameEl.textContent = DRV.name || 'Conductor'; if (plateEl) plateEl.textContent = [DRV.vehicle_type, DRV.vehicle_plate].filter(Boolean).join(' • '); if (photoEl && DRV.photo) { photoEl.src = DRV.photo; } else if (photoEl) { photoEl.style.display = 'none'; } const nameTrip = el('abd-driver-name-trip'); const plateTrip = el('abd-driver-vehicle-trip'); const photoTrip = el('abd-driver-avatar-trip'); if (nameTrip) nameTrip.textContent = DRV.name || 'Conductor'; if (plateTrip) plateTrip.textContent = [DRV.vehicle_type, DRV.vehicle_plate].filter(Boolean).join(' • '); if (photoTrip && DRV.photo) photoTrip.src = DRV.photo; } /* ── Connect toggle ─────────────────────────────────────────── */ function initToggle() { const toggle = el('abd-online-toggle'); if (!toggle) return; toggle.addEventListener('change', async function () { const online = this.checked; toggle.disabled = true; try { await api('/driver/status', 'POST', { online, lat: S.lat, lng: S.lng }); S.online = online; updateStatusLabel(online); if (online) { startGPS(); startPolls(); toast('¡Conectado! Esperando viajes...', 'success'); } else { stopGPS(); stopPolls(); toast('Desconectado.', 'info'); } } catch (e) { toggle.checked = !online; toast('Error al cambiar estado.', 'error'); } toggle.disabled = false; }); } function updateStatusLabel(online) { const lbl = el('abd-status-label'); if (!lbl) return; if (online) { lbl.textContent = 'ONLINE'; lbl.className = 'abd-status-label abd-status--online'; } else { lbl.textContent = 'OFFLINE'; lbl.className = 'abd-status-label abd-status--offline'; } } /* ── GPS ─────────────────────────────────────────────────────── */ function startGPS() { if (!navigator.geolocation) return; if (S.watchId !== null) navigator.geolocation.clearWatch(S.watchId); S.watchId = navigator.geolocation.watchPosition( pos => { S.lat = pos.coords.latitude; S.lng = pos.coords.longitude; S.accuracy = Math.round(pos.coords.accuracy); updateGPSBadge(); updateDriverMarker(); }, null, { enableHighAccuracy: true, maximumAge: 5000, timeout: 10000 } ); // Telemetry — 60s sin viaje, 20s con viaje activo S.telemetryInterval = setInterval(sendTelemetry, 60000); } function stopGPS() { if (S.watchId !== null) { navigator.geolocation.clearWatch(S.watchId); S.watchId = null; } if (S.telemetryInterval) { clearInterval(S.telemetryInterval); S.telemetryInterval = null; } } function updateGPSBadge() { const badge = el('abd-gps-badge'); if (badge) badge.textContent = S.accuracy !== null ? 'GPS: \u00b1' + S.accuracy + 'm' : 'GPS: \u2014'; } async function sendTelemetry() { if (S.lat === null) return; try { await api('/telemetry/position', 'POST', { lat: S.lat, lng: S.lng, accuracy: S.accuracy || 0, trip_id: S.trip ? S.trip.id : undefined, }); } catch (_) {} } /* ── Google Maps ─────────────────────────────────────────────── */ function loadGoogleMaps() { return new Promise(resolve => { if (window.google && window.google.maps) { resolve(); return; } window._abdMapsReady = resolve; const s = document.createElement('script'); s.src = 'https://maps.googleapis.com/maps/api/js?key=' + GMAPS_KEY + '&callback=_abdMapsReady'; s.async = true; document.head.appendChild(s); }); } /* ── Temas del mapa ─────────────────────────────────────────── */ const ABD_THEMES = { dark: [ { elementType: 'geometry', stylers: [{ color: '#1a1a2e' }] }, { elementType: 'labels.text.stroke', stylers: [{ color: '#1a1a2e' }] }, { elementType: 'labels.text.fill', stylers: [{ color: '#8a8a9a' }] }, { featureType: 'road', elementType: 'geometry', stylers: [{ color: '#0f3460' }] }, { featureType: 'road', elementType: 'geometry.stroke', stylers: [{ color: '#16213e' }] }, { featureType: 'road', elementType: 'labels.text.fill', stylers: [{ color: '#9ca5b3' }] }, { featureType: 'road.highway', elementType: 'geometry', stylers: [{ color: '#1a4a6e' }] }, { featureType: 'water', elementType: 'geometry', stylers: [{ color: '#0b1929' }] }, { featureType: 'poi', elementType: 'geometry', stylers: [{ color: '#151e2d' }] }, { featureType: 'transit', elementType: 'geometry', stylers: [{ color: '#2f3948' }] }, { featureType: 'administrative', elementType: 'geometry.stroke', stylers: [{ color: '#334155' }] }, ], normal: [], }; function getAutoTheme() { const h = new Date().getHours(); return (h >= 6 && h < 19) ? 'normal' : 'dark'; } function applyDriverMapTheme(theme) { const resolved = theme === 'auto' ? getAutoTheme() : theme; if (S.map) S.map.setOptions({ styles: ABD_THEMES[resolved] || [] }); localStorage.setItem('abd_map_theme', theme); document.querySelectorAll('.abd-theme-btn').forEach(b => { b.classList.toggle('active', b.dataset.theme === theme); }); } function initMap() { const mapEl = el('abd-map'); if (!mapEl || !window.google) return; const savedTheme = localStorage.getItem('abd_map_theme') || 'auto'; const resolved = savedTheme === 'auto' ? getAutoTheme() : savedTheme; const center = { lat: S.lat || 4.7110, lng: S.lng || -74.0721 }; S.map = new window.google.maps.Map(mapEl, { center, zoom: 14, styles: ABD_THEMES[resolved] || [], disableDefaultUI: true, zoomControl: true, zoomControlOptions: { position: window.google.maps.ControlPosition.RIGHT_CENTER }, gestureHandling: 'greedy', }); // Botones de tema document.querySelectorAll('.abd-theme-btn').forEach(b => { b.classList.toggle('active', b.dataset.theme === savedTheme); b.addEventListener('click', () => applyDriverMapTheme(b.dataset.theme)); }); // Driver marker S.driverMarker = new window.google.maps.Marker({ position: center, map: S.map, icon: { path: window.google.maps.SymbolPath.CIRCLE, scale: 10, fillColor: '#FF6F00', fillOpacity: 1, strokeColor: '#fff', strokeWeight: 2, }, title: 'Tu posición', }); } function updateDriverMarker() { if (!S.map || S.lat === null) return; const pos = { lat: S.lat, lng: S.lng }; if (S.driverMarker) S.driverMarker.setPosition(pos); if (!S.trip) S.map.panTo(pos); } function setDestMarker(lat, lng) { if (!S.map) return; const pos = { lat: parseFloat(lat), lng: parseFloat(lng) }; if (S.destMarker) { S.destMarker.setPosition(pos); } else { S.destMarker = new window.google.maps.Marker({ position: pos, map: S.map, icon: { path: window.google.maps.SymbolPath.BACKWARD_CLOSED_ARROW, scale: 8, fillColor: '#22C55E', fillOpacity: 1, strokeColor: '#fff', strokeWeight: 2, }, title: 'Destino', }); } } function drawTripRoute(trip) { if (!S.map) return; clearRoute(); const pickup = { lat: parseFloat(trip.pickup_lat), lng: parseFloat(trip.pickup_lng) }; const dropoff = { lat: parseFloat(trip.dropoff_lat), lng: parseFloat(trip.dropoff_lng) }; if (trip.route_polyline) { const path = window.google.maps.geometry ? window.google.maps.geometry.encoding.decodePath(trip.route_polyline) : [pickup, dropoff]; S.routePolyline = new window.google.maps.Polyline({ path, map: S.map, strokeColor: '#3b82f6', strokeWeight: 4, strokeOpacity: .85, }); } else { S.routePolyline = new window.google.maps.Polyline({ path: [pickup, dropoff], map: S.map, strokeColor: '#3b82f6', strokeWeight: 4, strokeOpacity: .7, geodesic: true, }); } setDestMarker(trip.dropoff_lat, trip.dropoff_lng); const bounds = new window.google.maps.LatLngBounds(); if (S.lat) bounds.extend({ lat: S.lat, lng: S.lng }); bounds.extend(pickup); bounds.extend(dropoff); S.map.fitBounds(bounds, { top: 40, right: 20, bottom: 20, left: 20 }); } function clearRoute() { if (S.routePolyline) { S.routePolyline.setMap(null); S.routePolyline = null; } if (S.suggestPolyline) { S.suggestPolyline.setMap(null); S.suggestPolyline = null; } if (S.destMarker) { S.destMarker.setMap(null); S.destMarker = null; } } function initRouteBanner() { const accept = el('abd-route-accept'); const keep = el('abd-route-keep'); const banner = el('abd-route-banner'); if (accept) accept.addEventListener('click', () => { if (S.routePolyline && S.suggestPolyline) { S.routePolyline.setMap(null); S.routePolyline = S.suggestPolyline; S.suggestPolyline = null; } if (banner) banner.style.display = 'none'; }); if (keep) keep.addEventListener('click', () => { if (S.suggestPolyline) { S.suggestPolyline.setMap(null); S.suggestPolyline = null; } if (banner) banner.style.display = 'none'; }); } /* ── Polls ───────────────────────────────────────────────────── */ function startPolls() { if (!S.tripPollInterval) S.tripPollInterval = setInterval(pollTrip, 8000); if (!S.zonePollInterval) S.zonePollInterval = setInterval(pollZones, 30000); pollTrip(); pollZones(); } function stopPolls() { if (S.tripPollInterval) { clearInterval(S.tripPollInterval); S.tripPollInterval = null; } if (S.chatPollInterval) { clearInterval(S.chatPollInterval); S.chatPollInterval = null; } if (S.zonePollInterval) { clearInterval(S.zonePollInterval); S.zonePollInterval = null; } } function startChatPoll(rideId) { if (S.chatPollInterval) clearInterval(S.chatPollInterval); S.chatPollInterval = setInterval(() => pollChat(rideId), 3000); pollChat(rideId); } /* ── Trip poll ───────────────────────────────────────────────── */ async function pollTrip() { try { const trip = await api('/driver/current-trip'); if (trip && trip.id) { if (!S.trip || S.trip.id !== trip.id || S.tripStatus !== trip.status) { showTrip(trip); } else { S.trip = trip; updateUnreadBadge(trip.unread_count || 0); } } else { if (S.trip) hideTrip(); } } catch (_) {} } /* ── Trip state machine ──────────────────────────────────────── */ function setTelemetryInterval(ms) { if (S.telemetryInterval) { clearInterval(S.telemetryInterval); S.telemetryInterval = null; } S.telemetryInterval = setInterval(sendTelemetry, ms); } function showTrip(trip) { const prevStatus = S.tripStatus; S.trip = trip; S.tripStatus = trip.status; setTelemetryInterval(20000); if (trip.status === 'finished') { hideTrip(); toast('¡Viaje completado! Bien hecho. 🎉', 'success'); return; } if (trip.status === 'canceled') { hideTrip(); toast('Viaje cancelado.', 'warn'); return; } el('abd-no-trip').style.display = 'none'; el('abd-active-trip').style.display = ''; // Passenger const passName = el('abd-pass-name'); const passPhoto = el('abd-pass-photo'); if (passName) passName.textContent = trip.passenger_name || 'Pasajero'; if (passPhoto) { passPhoto.src = trip.passenger_photo || ''; passPhoto.style.display = trip.passenger_photo ? '' : 'none'; } // Status badge const badge = el('abd-trip-status-badge'); if (badge) { badge.textContent = statusLabel(trip.status); badge.className = 'abd-trip-status-badge ' + statusBadgeClass(trip.status); } // Origin / Dest const origin = el('abd-origin'); const dest = el('abd-dest'); if (origin) origin.textContent = coordStr(trip.pickup_lat, trip.pickup_lng); if (dest) dest.textContent = coordStr(trip.dropoff_lat, trip.dropoff_lng); // Distance const distEl = el('abd-distance'); if (distEl && trip.fare_distance_m > 0) { const km = (trip.fare_distance_m / 1000).toFixed(1); const mi = (trip.fare_distance_m / 1609.34).toFixed(1); distEl.textContent = km + ' km (' + mi + ' mi)'; } else if (distEl) { distEl.textContent = '—'; } // Fare const fareEl = el('abd-fare'); if (fareEl) { const cur = trip.currency || 'USD'; fareEl.textContent = cur + ' ' + parseFloat(trip.fare_total_amount || 0).toFixed(2); } // Instructions const instrEl = el('abd-trip-instructions'); if (instrEl) instrEl.textContent = tripInstruction(trip.status); // Courtesy timer if (trip.status === 'waiting') { if (prevStatus !== 'waiting') { S.courtesySecondsLeft = 180; startCourtesyTimer(); } } else { stopCourtesyTimer(); } // SOS button const sosBtn = el('abd-sos-btn'); if (sosBtn) sosBtn.style.display = trip.status === 'in_progress' ? '' : 'none'; // Chat const chatDiv = el('abd-chat'); if (chatDiv) chatDiv.style.display = ''; startChatPoll(trip.id); // Map route if (trip.pickup_lat && trip.dropoff_lat) drawTripRoute(trip); // Action buttons renderButtons(trip.status, trip); } function hideTrip() { S.trip = null; S.tripStatus = null; setTelemetryInterval(60000); el('abd-no-trip').style.display = ''; el('abd-active-trip').style.display = 'none'; const chatDiv = el('abd-chat'); if (chatDiv) chatDiv.style.display = 'none'; if (S.chatPollInterval) { clearInterval(S.chatPollInterval); S.chatPollInterval = null; } stopCourtesyTimer(); clearRoute(); el('abd-trip-actions').innerHTML = ''; S.chatLastId = 0; S.unreadChat = 0; } function renderButtons(status, trip) { const box = el('abd-trip-actions'); if (!box) return; box.innerHTML = ''; function mkBtn(label, cls, onClick) { const b = document.createElement('button'); b.className = 'abd-btn ' + cls; b.textContent = label; b.onclick = onClick; box.appendChild(b); } if (status === 'assigned') { mkBtn('✅ Aceptar viaje', 'abd-btn--brand', async () => { box.querySelectorAll('button').forEach(b => { b.disabled = true; }); try { await api('/trip/accept', 'POST', { trip_uuid: trip.trip_uuid }); toast('¡En camino al pasajero!', 'success'); pollTrip(); } catch (e) { toast('Error al aceptar viaje.', 'error'); box.querySelectorAll('button').forEach(b => { b.disabled = false; }); } }); } else if (status === 'en_route') { mkBtn('📍 Ya llegué', 'abd-btn--brand', async () => { box.querySelectorAll('button').forEach(b => { b.disabled = true; }); try { await api('/trip/arrive', 'POST', { trip_uuid: trip.trip_uuid }); toast('¡Esperando al pasajero!', 'success'); pollTrip(); } catch (e) { toast('Error.', 'error'); box.querySelectorAll('button').forEach(b => { b.disabled = false; }); } }); mkBtn('❌ Cancelar viaje', 'abd-btn--ghost', () => confirmCancel(trip)); } else if (status === 'waiting') { mkBtn('🚗 Iniciar viaje', 'abd-btn--brand', async () => { box.querySelectorAll('button').forEach(b => { b.disabled = true; }); try { await api('/trip/start', 'POST', { trip_uuid: trip.trip_uuid }); toast('¡Viaje iniciado!', 'success'); stopCourtesyTimer(); pollTrip(); } catch (e) { toast('Error al iniciar viaje.', 'error'); box.querySelectorAll('button').forEach(b => { b.disabled = false; }); } }); mkBtn('❌ Cancelar viaje', 'abd-btn--ghost', () => confirmCancel(trip)); } else if (status === 'in_progress') { mkBtn('🏁 Finalizar viaje', 'abd-btn--brand', () => openFinishModal(trip)); } } function confirmCancel(trip) { if (!confirm('¿Cancelar este viaje?')) return; api('/trip/cancel', 'POST', { trip_uuid: trip.trip_uuid }) .then(() => { toast('Viaje cancelado.', 'info'); pollTrip(); }) .catch(() => toast('Error al cancelar.', 'error')); } function tripInstruction(status) { const map = { assigned: 'Nuevo viaje asignado. Presiona ACEPTAR para confirmar.', en_route: 'En camino al pasajero. Presiona LLEGUÉ cuando estés en el punto.', waiting: 'Esperando al pasajero. Inicia el viaje cuando suba al vehículo.', in_progress: 'Viaje en curso. Finaliza cuando llegues al destino.', finished: '¡Viaje completado! Bien hecho.', canceled: 'Viaje cancelado.', }; return map[status] || ''; } function statusLabel(status) { const map = { assigned: 'Asignado', en_route: 'En camino', waiting: 'Esperando', in_progress: 'En progreso', finished: 'Finalizado', canceled: 'Cancelado', }; return map[status] || status; } function statusBadgeClass(status) { const map = { assigned: 'abd-status-badge abd-status-badge--assigned', en_route: 'abd-status-badge abd-status-badge--progress', waiting: 'abd-status-badge abd-status-badge--waiting', in_progress: 'abd-status-badge abd-status-badge--progress', finished: 'abd-status-badge abd-status-badge--finished', canceled: 'abd-status-badge abd-status-badge--canceled', }; return map[status] || 'abd-status-badge'; } function coordStr(lat, lng) { if (!lat || !lng) return '—'; return parseFloat(lat).toFixed(4) + ', ' + parseFloat(lng).toFixed(4); } /* ── Courtesy timer ──────────────────────────────────────────── */ function startCourtesyTimer() { stopCourtesyTimer(); const wrap = el('abd-courtesy-timer'); const val = el('abd-courtesy-val'); if (wrap) wrap.style.display = ''; function tick() { if (!val) return; const m = Math.floor(S.courtesySecondsLeft / 60); const s = S.courtesySecondsLeft % 60; val.textContent = m + ':' + (s < 10 ? '0' : '') + s; if (S.courtesySecondsLeft <= 0) { stopCourtesyTimer(); toast('Tiempo de cortesía expirado.', 'warn'); } S.courtesySecondsLeft--; } tick(); S.courtesyInterval = setInterval(tick, 1000); } function stopCourtesyTimer() { if (S.courtesyInterval) { clearInterval(S.courtesyInterval); S.courtesyInterval = null; } const wrap = el('abd-courtesy-timer'); if (wrap) wrap.style.display = 'none'; } /* ── Finish modal ────────────────────────────────────────────── */ function openFinishModal(trip) { el('abd-tolls-input').value = '0'; el('abd-tips-input').value = '0'; el('abd-finish-modal').style.display = 'flex'; el('abd-finish-confirm').onclick = async () => { const tolls = parseFloat(el('abd-tolls-input').value) || 0; const tips = parseFloat(el('abd-tips-input').value) || 0; el('abd-finish-confirm').disabled = true; try { await api('/trip/finish', 'POST', { trip_uuid: trip.trip_uuid, tolls_amount: tolls, tips_amount: tips, }); el('abd-finish-modal').style.display = 'none'; toast('¡Viaje finalizado! 🎉', 'success'); hideTrip(); if (S.online) loadEarnings('day', false); } catch (e) { toast('Error al finalizar viaje.', 'error'); } el('abd-finish-confirm').disabled = false; }; el('abd-finish-cancel').onclick = () => { el('abd-finish-modal').style.display = 'none'; }; } /* ── Chat ────────────────────────────────────────────────────── */ async function pollChat(rideId) { if (!rideId) return; try { const msgs = await api('/chat/list?ride_id=' + rideId + '&after_id=' + S.chatLastId); if (msgs && msgs.length) { msgs.forEach(m => appendChatBubble(m)); S.chatLastId = msgs[msgs.length - 1].id; if (!S.chatOpen) { S.unreadChat += msgs.filter(m => m.sender !== 'driver').length; updateUnreadBadge(S.unreadChat); } const msgsDiv = el('abd-chat-msgs'); if (msgsDiv) msgsDiv.scrollTop = msgsDiv.scrollHeight; } } catch (_) {} } function appendChatBubble(m) { const msgsDiv = el('abd-chat-msgs'); if (!msgsDiv) return; const isDriver = m.sender === 'driver'; const div = document.createElement('div'); div.className = 'abd-chat-bubble ' + (isDriver ? 'abd-chat-bubble--driver' : 'abd-chat-bubble--customer'); const timeStr = m.created_at ? new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; div.innerHTML = escapeHtml(m.message) + '' + timeStr + ''; msgsDiv.appendChild(div); } function updateUnreadBadge(count) { S.unreadChat = count; const badge = el('abd-chat-badge'); if (!badge) return; if (count > 0) { badge.textContent = count; badge.style.display = ''; } else { badge.style.display = 'none'; } } function initChat() { const sendBtn = el('abd-chat-send'); const input = el('abd-chat-input'); if (sendBtn) sendBtn.addEventListener('click', sendChat); if (input) input.addEventListener('keypress', e => { if (e.key === 'Enter') sendChat(); }); const hdr = el('abd-chat-toggle-header'); if (hdr) hdr.addEventListener('click', () => { S.chatOpen = !S.chatOpen; const body = el('abd-chat-body'); if (body) body.style.display = S.chatOpen ? '' : 'none'; if (S.chatOpen) { S.unreadChat = 0; updateUnreadBadge(0); } }); } async function sendChat() { const input = el('abd-chat-input'); if (!input || !S.trip) return; const msg = input.value.trim(); if (!msg) return; input.value = ''; try { await api('/chat/send', 'POST', { ride_id: S.trip.id, message: msg, sender: 'driver' }); } catch (e) { toast('Error al enviar mensaje.', 'error'); } } /* ── Zone alerts ─────────────────────────────────────────────── */ async function pollZones() { try { const zones = await api('/driver/zones'); if (zones && zones.length > 0) { showZoneCard(zones[0]); } else { hideZoneCard(); } } catch (_) {} } function showZoneCard(zone) { if (S.activeZoneId === zone.id) return; S.activeZoneId = zone.id; el('abd-zone-card').style.display = ''; el('abd-zone-title').textContent = zone.title || ''; el('abd-zone-desc').textContent = zone.description || ''; const typeBadge = el('abd-zone-type-badge'); if (typeBadge) typeBadge.textContent = (zone.type || 'hotspot').toUpperCase(); const bonusWrap = el('abd-zone-bonus'); const bonusVal = el('abd-zone-bonus-val'); if (zone.bonus_amount > 0) { bonusWrap.style.display = ''; bonusVal.textContent = zone.bonus_currency + ' ' + parseFloat(zone.bonus_amount).toFixed(2); } else { bonusWrap.style.display = 'none'; } // Countdown let secs = parseInt(zone.countdown_seconds) || 180; if (S.zoneTimers[zone.id]) clearInterval(S.zoneTimers[zone.id]); const cdEl = el('abd-zone-countdown'); function renderCountdown() { const m = Math.floor(secs / 60); const s = secs % 60; if (cdEl) cdEl.textContent = m + ':' + (s < 10 ? '0' : '') + s; if (secs <= 0) { clearInterval(S.zoneTimers[zone.id]); api('/driver/zone-response', 'POST', { zone_id: zone.id, response: 'not_going' }).catch(() => {}); hideZoneCard(); } secs--; } renderCountdown(); S.zoneTimers[zone.id] = setInterval(renderCountdown, 1000); // Buttons const goBtn = el('abd-zone-going'); const noBtn = el('abd-zone-no'); goBtn.onclick = async () => { try { await api('/driver/zone-response', 'POST', { zone_id: zone.id, response: 'going' }); toast('¡En camino a la zona!', 'success'); } catch (_) {} clearInterval(S.zoneTimers[zone.id]); hideZoneCard(); }; noBtn.onclick = async () => { try { await api('/driver/zone-response', 'POST', { zone_id: zone.id, response: 'not_going' }); } catch (_) {} clearInterval(S.zoneTimers[zone.id]); hideZoneCard(); }; } function hideZoneCard() { S.activeZoneId = null; const card = el('abd-zone-card'); if (card) card.style.display = 'none'; } /* ── Earnings ────────────────────────────────────────────────── */ async function loadEarnings(period, loadYear) { S.earnPeriod = period; try { const data = await api('/driver/earnings?period=' + period); el('abd-earn-gross').textContent = '$' + data.gross.toFixed(2); el('abd-earn-fee').textContent = '-$' + data.fee.toFixed(2); el('abd-earn-net').textContent = '$' + data.net.toFixed(2); el('abd-earn-trips').textContent = data.trips_count + ' viaje' + (data.trips_count !== 1 ? 's' : '') + ' · ' + data.distance_km + ' km'; renderBarChart(data.last_7_days || []); } catch (_) { toast('Error al cargar ganancias.', 'error'); } if (loadYear || !S.yearData) { try { const yData = await api('/driver/earnings?period=year'); S.yearData = yData; el('abd-tax-gross').textContent = '$' + yData.gross.toFixed(2); el('abd-tax-fee').textContent = '-$' + yData.fee.toFixed(2); el('abd-tax-net').textContent = '$' + yData.net.toFixed(2); } catch (_) {} } } function renderBarChart(days) { const container = el('abd-bar-chart'); if (!container) return; container.innerHTML = ''; const max = Math.max(...days.map(d => d.net), 0.01); const dayNames = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']; days.forEach(d => { const col = document.createElement('div'); col.className = 'abd-bar-col'; const bar = document.createElement('div'); bar.className = 'abd-bar'; const pct = Math.max(4, Math.round((d.net / max) * 100)); bar.style.height = pct + 'px'; const tip = document.createElement('div'); tip.className = 'abd-bar-tooltip'; tip.textContent = '$' + parseFloat(d.net).toFixed(2); bar.appendChild(tip); const lbl = document.createElement('div'); lbl.className = 'abd-bar-label'; const dt = new Date(d.date + 'T00:00:00'); lbl.textContent = dayNames[dt.getDay()]; col.appendChild(bar); col.appendChild(lbl); container.appendChild(col); }); } function initPeriodTabs() { document.querySelectorAll('.abd-period-tab').forEach(btn => { btn.addEventListener('click', function () { document.querySelectorAll('.abd-period-tab').forEach(b => b.classList.remove('abd-period-tab--active')); this.classList.add('abd-period-tab--active'); loadEarnings(this.dataset.period, false); }); }); } function initExportCSV() { const btn = el('abd-export-csv'); if (btn) btn.addEventListener('click', exportEarningsCSV); } async function exportEarningsCSV() { try { const data = await api('/driver/history?per_page=1000'); const rows = data.rows || data.trips || []; let csv = '\uFEFF' + 'Fecha,Pasajero,Bruto,Comisión,Neto,Estado\r\n'; rows.forEach(t => { const date = (t.created_at || '').slice(0, 10); csv += [ date, '"' + (t.passenger_name || '').replace(/"/g, '""') + '"', parseFloat(t.fare_total_amount || 0).toFixed(2), parseFloat(t.platform_fee_amount || 0).toFixed(2), parseFloat(t.driver_payout_amount || 0).toFixed(2), t.status || '', ].join(',') + '\r\n'; }); const today = new Date().toISOString().slice(0, 10); downloadCSV(csv, 'ganancias_' + today + '.csv'); } catch (_) { toast('Error al exportar.', 'error'); } } function downloadCSV(content, filename) { const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } /* ── Metrics ─────────────────────────────────────────────────── */ async function loadMetrics() { try { const m = await api('/driver/metrics'); setBar('bar-acceptance', 'val-acceptance', m.acceptance_rate, m.acceptance_rate + '%'); setBar('bar-completion', 'val-completion', m.completion_rate, m.completion_rate + '%'); renderStars(m.avg_rating || 0); el('val-rating').textContent = m.avg_rating ? m.avg_rating.toFixed(1) + '★' : '—'; const maxHours = 60; const hrPct = Math.min(100, (m.online_hours_week / maxHours) * 100); setBar('bar-hours', 'val-hours', hrPct, m.online_hours_week + 'h'); const total = m.zones_responded + m.zones_ignored; const zpct = total > 0 ? Math.round((m.zones_responded / total) * 100) : 0; setBar('bar-zones', 'val-zones', zpct, m.zones_responded + '/' + total); } catch (_) { toast('Error al cargar métricas.', 'error'); } } function setBar(barId, valId, pct, label) { const bar = el(barId); const val = el(valId); if (bar) bar.style.width = Math.min(100, Math.max(0, pct)) + '%'; if (val) val.textContent = label; } function renderStars(rating) { const container = el('abd-stars'); if (!container) return; container.innerHTML = ''; for (let i = 1; i <= 5; i++) { const span = document.createElement('span'); span.textContent = '★'; span.className = i <= Math.round(rating) ? 'abd-star-fill' : 'abd-star-empty'; container.appendChild(span); } } /* ── History ─────────────────────────────────────────────────── */ async function loadHistory(page, from, to) { S.histPage = page || 1; S.histFrom = from || ''; S.histTo = to || ''; const tbody = el('abd-history-tbody'); if (tbody) tbody.innerHTML = 'Cargando...'; try { let url = '/driver/history?page=' + S.histPage + '&per_page=20'; if (S.histFrom) url += '&date_from=' + S.histFrom; if (S.histTo) url += '&date_to=' + S.histTo; const data = await api(url); S.historyData = data.rows || data.trips || []; renderHistoryTable(S.historyData); renderPagination(data.page, data.total_pages); } catch (_) { if (tbody) tbody.innerHTML = '
🚗
Sin viajes aún
'; } } function renderHistoryTable(trips) { const tbody = el('abd-history-tbody'); if (!tbody) return; if (!trips.length) { tbody.innerHTML = '
🚗
Sin viajes aún
'; return; } tbody.innerHTML = ''; trips.forEach(t => { const tr = document.createElement('tr'); const date = (t.created_at || '').slice(0, 10); const ruta = coordStr(t.pickup_lat, t.pickup_lng) + ' → ' + coordStr(t.dropoff_lat, t.dropoff_lng); const gross = '$' + parseFloat(t.fare_total_amount || 0).toFixed(2); const fee = '-$'+ parseFloat(t.platform_fee_amount || 0).toFixed(2); const net = '$' + parseFloat(t.driver_payout_amount || 0).toFixed(2); tr.innerHTML = [ '' + escapeHtml(date) + '', '' + escapeHtml(t.passenger_name || '—') + '', '' + escapeHtml(ruta) + '', '' + gross + '', '' + fee + '', '' + net + '', '' + statusLabel(t.status) + '', ].join(''); tbody.appendChild(tr); }); } function renderPagination(page, totalPages) { const box = el('abd-history-pagination'); if (!box) return; box.innerHTML = ''; if (totalPages <= 1) return; function mkBtn(label, pg, active, disabled) { const b = document.createElement('button'); b.className = 'abd-page-btn' + (active ? ' abd-page-btn--active' : ''); b.textContent = label; b.disabled = !!disabled; if (!disabled) b.onclick = () => loadHistory(pg, S.histFrom, S.histTo); box.appendChild(b); } mkBtn('← Ant', page - 1, false, page <= 1); const info = document.createElement('span'); info.className = 'abd-page-info'; info.textContent = 'Pág ' + page + ' / ' + totalPages; box.appendChild(info); mkBtn('Sig →', page + 1, false, page >= totalPages); } function initHistoryFilters() { const btn = el('abd-hist-filter'); if (btn) btn.addEventListener('click', () => { const from = el('abd-hist-from').value; const to = el('abd-hist-to').value; loadHistory(1, from, to); }); } /* ── Profile ─────────────────────────────────────────────────── */ async function loadProfile() { try { const p = await api('/driver/profile'); const photoEl = el('abd-prof-photo'); if (photoEl) { photoEl.src = p.photo_url || ''; photoEl.style.display = p.photo_url ? '' : 'none'; } setTextEl('abd-prof-name', p.name || '—'); setTextEl('abd-prof-vehicle', [p.vehicle_type, p.vehicle_plate].filter(Boolean).join(' • ') || '—'); setTextEl('abd-prof-email', p.email ? '✉ ' + p.email : ''); setTextEl('abd-prof-phone', p.phone ? '📞 ' + p.phone : ''); renderDocRow('doc-insurance', p.insurance_expiry, p.insurance_status); renderDocRow('doc-license', p.license_expiry, p.license_status); renderDocRow('doc-inspection', p.inspection_expiry, p.inspection_status); loadScheduled(); } catch (_) { toast('Error al cargar perfil.', 'error'); } } function renderDocRow(rowId, expiry, status) { const row = el(rowId); if (!row) return; const icon = row.querySelector('.abd-doc-icon'); const exp = row.querySelector('.abd-doc-expiry'); const badge = row.querySelector('.abd-doc-status-badge'); const icons = { ok: '✅', warning: '⚠️', expired: '❌', unknown: '❓' }; const labels= { ok: 'Vigente', warning: 'Por vencer', expired: 'Vencido', unknown: 'Sin dato' }; if (icon) icon.textContent = icons[status] || '❓'; if (exp) exp.textContent = expiry ? expiry : '—'; if (badge) { badge.textContent = labels[status] || '—'; badge.className = 'abd-doc-status-badge abd-doc-status-badge--' + (status || 'unknown'); } } async function loadScheduled() { const list = el('abd-scheduled-list'); if (!list) return; try { const trips = await api('/driver/scheduled'); if (!trips || !trips.length) { list.innerHTML = '

Sin viajes programados próximamente.

'; return; } list.innerHTML = ''; trips.forEach(t => { const div = document.createElement('div'); div.className = 'abd-scheduled-item'; const dt = t.pickup_time ? new Date(t.pickup_time).toLocaleString() : '—'; div.innerHTML = '
' + escapeHtml(dt) + '
' + '
' + '👤 ' + escapeHtml(t.passenger_name || '—') + '
' + '📍 ' + escapeHtml(t.pickup_address || '—') + ' → ' + escapeHtml(t.dropoff_address || '—') + '
' + '💰 ' + (t.currency || '') + ' ' + parseFloat(t.fare_total_amount || t.estimated_fare || 0).toFixed(2) + '
'; list.appendChild(div); }); } catch (_) {} } /* ── Profile edit & change-password ─────────────────────────── */ function initProfile() { const btnEdit = el('abd-btn-edit-profile'); const btnPwd = el('abd-btn-change-password'); if (btnEdit) btnEdit.addEventListener('click', openEditProfile); if (btnPwd) btnPwd.addEventListener('click', openChangePwd); // Change-password modal const pwdModal = el('abd-pwd-modal'); const pwdSave = el('abd-pwd-save'); const pwdCancel = el('abd-pwd-cancel'); if (pwdCancel) pwdCancel.addEventListener('click', () => { pwdModal.style.display = 'none'; }); if (pwdModal) pwdModal.addEventListener('click', e => { if (e.target === pwdModal) pwdModal.style.display = 'none'; }); if (pwdSave) pwdSave.addEventListener('click', doChangePwd); // Edit-profile modal const epModal = el('abd-editprofile-modal'); const epSave = el('abd-ep-save'); const epCancel = el('abd-ep-cancel'); if (epCancel) epCancel.addEventListener('click', () => { epModal.style.display = 'none'; }); if (epModal) epModal.addEventListener('click', e => { if (e.target === epModal) epModal.style.display = 'none'; }); if (epSave) epSave.addEventListener('click', doUpdateProfile); // Photo file picker const photoBtn = el('abd-ep-photo-btn'); const photoFile = el('abd-ep-photo-file'); const photoPreview = el('abd-ep-photo-preview'); if (photoBtn && photoFile) { photoBtn.addEventListener('click', () => photoFile.click()); photoFile.addEventListener('change', () => { const file = photoFile.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { el('abd-ep-photo-url').value = ''; if (photoPreview) { photoPreview.src = e.target.result; photoPreview.style.display = ''; } }; reader.readAsDataURL(file); }); } // URL field preview const photoUrl = el('abd-ep-photo-url'); if (photoUrl) { photoUrl.addEventListener('input', () => { if (photoPreview && photoUrl.value) { photoPreview.src = photoUrl.value; photoPreview.style.display = ''; } }); } } function openChangePwd() { const m = el('abd-pwd-modal'); if (!m) return; ['abd-pwd-current','abd-pwd-new','abd-pwd-confirm'].forEach(id => { const i = el(id); if(i) i.value = ''; }); const s = el('abd-pwd-status'); if(s) s.textContent = ''; m.style.display = 'flex'; } async function doChangePwd() { const btn = el('abd-pwd-save'); const status = el('abd-pwd-status'); const current = el('abd-pwd-current')?.value || ''; const newPw = el('abd-pwd-new')?.value || ''; const confirm = el('abd-pwd-confirm')?.value || ''; if (!current || !newPw || !confirm) { status.textContent = 'Completa todos los campos.'; return; } if (newPw !== confirm) { status.textContent = 'Las contraseñas no coinciden.'; return; } if (newPw.length < 8) { status.textContent = 'Mínimo 8 caracteres.'; return; } btn.disabled = true; btn.textContent = 'Guardando…'; status.textContent = ''; try { const r = await api('/driver/change-password', 'POST', { current_password: current, new_password: newPw, confirm_password: confirm }); status.style.color = 'var(--ab-green)'; status.textContent = '✅ ' + (r.msg || 'Contraseña actualizada.'); setTimeout(() => { el('abd-pwd-modal').style.display = 'none'; }, 1800); } catch(e) { status.style.color = 'var(--ab-red)'; status.textContent = e.message || 'Error al cambiar contraseña.'; } finally { btn.disabled = false; btn.textContent = 'Guardar'; } } async function openEditProfile() { const m = el('abd-editprofile-modal'); if (!m) return; el('abd-ep-status').textContent = ''; try { const p = await api('/driver/profile'); const phoneEl = el('abd-ep-phone'); if(phoneEl) phoneEl.value = p.phone || ''; const vehicleEl = el('abd-ep-vehicle'); if(vehicleEl) vehicleEl.value = p.vehicle_type || ''; const plateEl = el('abd-ep-plate'); if(plateEl) plateEl.value = p.vehicle_plate || ''; const urlEl = el('abd-ep-photo-url'); if(urlEl) urlEl.value = ''; const preview = el('abd-ep-photo-preview'); if (preview && p.photo_url) { preview.src = p.photo_url; preview.style.display = ''; } else if (preview) { preview.style.display = 'none'; } } catch(_) {} m.style.display = 'flex'; } async function doUpdateProfile() { const btn = el('abd-ep-save'); const status = el('abd-ep-status'); btn.disabled = true; btn.textContent = 'Guardando…'; status.textContent = ''; // Determine photo: file upload (base64) or URL let photoUrl = el('abd-ep-photo-url')?.value?.trim() || ''; const photoFile = el('abd-ep-photo-file'); if (!photoUrl && photoFile && photoFile.files[0]) { photoUrl = await new Promise(res => { const r = new FileReader(); r.onload = e => res(e.target.result); r.readAsDataURL(photoFile.files[0]); }); } const body = { phone: el('abd-ep-phone')?.value.trim() || '', vehicle_type: el('abd-ep-vehicle')?.value.trim() || '', vehicle_plate: el('abd-ep-plate')?.value.trim() || '', photo_url: photoUrl, }; try { const r = await api('/driver/update-profile', 'POST', body); status.style.color = 'var(--ab-green)'; status.textContent = '✅ ' + (r.msg || 'Perfil actualizado.'); // Reload profile display loadProfile(); setTimeout(() => { el('abd-editprofile-modal').style.display = 'none'; }, 1800); } catch(e) { status.style.color = 'var(--ab-red)'; status.textContent = e.message || 'Error al guardar.'; } finally { btn.disabled = false; btn.textContent = 'Guardar cambios'; } } /* ── SOS / Panic ─────────────────────────────────────────────── */ function initSOS() { const sosBtn = el('abd-sos-btn'); const modal = el('abd-sos-modal'); const confirm = el('abd-sos-confirm'); const cancelBtn = el('abd-sos-cancel'); const stopBtn = el('abd-sos-stop'); if (sosBtn) sosBtn.addEventListener('click', () => { if (modal) modal.style.display = 'flex'; startMediaPreview(); }); if (cancelBtn) cancelBtn.addEventListener('click', () => { modal.style.display = 'none'; stopMediaPreview(); }); if (confirm) confirm.addEventListener('click', activateSOS); if (stopBtn) stopBtn.addEventListener('click', stopSOS); } async function startMediaPreview() { const video = el('abd-sos-video'); const status = el('abd-sos-rec-status'); try { S.mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); if (video) { video.srcObject = S.mediaStream; video.style.display = ''; } if (status) status.textContent = 'Cámara y micrófono listos.'; } catch (_) { try { S.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); if (video) video.style.display = 'none'; if (status) status.textContent = 'Solo audio disponible (sin cámara).'; } catch (e2) { if (status) status.textContent = 'Sin acceso a medios. Se enviará solo ubicación GPS.'; } } } function stopMediaPreview() { if (S.mediaStream) { S.mediaStream.getTracks().forEach(t => t.stop()); S.mediaStream = null; } const video = el('abd-sos-video'); if (video) { video.srcObject = null; video.style.display = 'none'; } } async function activateSOS() { const modal = el('abd-sos-modal'); const status = el('abd-sos-rec-status'); if (status) status.textContent = 'Enviando alerta...'; el('abd-sos-confirm').disabled = true; try { const body = { lat: S.lat || 0, lng: S.lng || 0 }; if (S.trip) body.trip_uuid = S.trip.trip_uuid; const res = await api('/incident/sos', 'POST', body); S.incidentId = res.incident_id; S.sosChunkIndex = 0; if (modal) modal.style.display = 'none'; el('abd-sos-badge').style.display = ''; toast('🚨 Alerta enviada. Admin notificado.', 'error'); if (S.mediaStream) { startMediaRecording(); } } catch (e) { if (status) status.textContent = 'Error al enviar alerta.'; toast('Error al activar SOS.', 'error'); } el('abd-sos-confirm').disabled = false; } function startMediaRecording() { if (!S.mediaStream || !S.incidentId) return; try { const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : ''; S.mediaRec = new MediaRecorder(S.mediaStream, mimeType ? { mimeType } : {}); let chunks = []; S.mediaRec.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); }; S.mediaRec.onstop = async () => { if (!chunks.length || !S.incidentId) return; const blob = new Blob(chunks, { type: mimeType || 'audio/webm' }); const reader = new FileReader(); reader.onloadend = async () => { const b64 = reader.result.split(',')[1]; try { await api('/incident/sos', 'POST', { incident_id: S.incidentId, chunk_index: S.sosChunkIndex, audio_data: b64, }); } catch (_) {} S.sosChunkIndex++; chunks = []; if (S.incidentId && S.mediaRec) { S.mediaRec.start(); setTimeout(() => { if (S.mediaRec && S.mediaRec.state === 'recording') S.mediaRec.stop(); }, 5000); } }; reader.readAsDataURL(blob); }; S.mediaRec.start(); setTimeout(() => { if (S.mediaRec && S.mediaRec.state === 'recording') S.mediaRec.stop(); }, 5000); } catch (_) {} } function stopSOS() { S.incidentId = null; if (S.mediaRec && S.mediaRec.state !== 'inactive') { try { S.mediaRec.stop(); } catch (_) {} } S.mediaRec = null; stopMediaPreview(); el('abd-sos-badge').style.display = 'none'; toast('Alerta SOS detenida.', 'info'); } /* ── Utilities ───────────────────────────────────────────────── */ function escapeHtml(str) { if (!str) return ''; return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function setTextEl(id, text) { const el2 = el(id); if (el2) el2.textContent = text; } /* ── Init ────────────────────────────────────────────────────── */ async function init() { // Populate header populateHeader(); // Init UI controls initTabs(); initToggle(); initChat(); initRouteBanner(); initPeriodTabs(); initExportCSV(); initHistoryFilters(); initProfile(); initSOS(); // Load Google Maps if (GMAPS_KEY) { try { await loadGoogleMaps(); initMap(); } catch (_) {} } // Load driver status try { const status = await api('/driver/status'); S.online = !!status.online; const toggle = el('abd-online-toggle'); if (toggle) toggle.checked = S.online; updateStatusLabel(S.online); if (S.online) { startGPS(); startPolls(); } } catch (_) {} // Hide init loader const loader = el('abd-init-loader'); if (loader) { loader.classList.add('abd-fade-out'); setTimeout(() => { loader.style.display = 'none'; }, 450); } } document.addEventListener('DOMContentLoaded', init); })();