/* 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 = '
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 = '