Files
AutoBooking/driver-dashboard.js

1405 lines
53 KiB
JavaScript

/* 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) + '<span class="abd-chat-time">' + timeStr + '</span>';
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 = '<tr><td colspan="7" style="text-align:center;padding:20px;color:rgba(255,255,255,.4);">Cargando...</td></tr>';
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 = '<tr><td colspan="7" style="text-align:center;padding:32px;"><div style="font-size:32px;margin-bottom:8px;">🚗</div><div style="color:rgba(255,255,255,.5);font-size:14px;">Sin viajes aún</div></td></tr>';
}
}
function renderHistoryTable(trips) {
const tbody = el('abd-history-tbody');
if (!tbody) return;
if (!trips.length) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:32px;"><div style="font-size:32px;margin-bottom:8px;">🚗</div><div style="color:rgba(255,255,255,.5);font-size:14px;">Sin viajes aún</div></td></tr>';
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 = [
'<td>' + escapeHtml(date) + '</td>',
'<td>' + escapeHtml(t.passenger_name || '—') + '</td>',
'<td style="font-size:11px;color:rgba(255,255,255,.5);">' + escapeHtml(ruta) + '</td>',
'<td>' + gross + '</td>',
'<td style="color:var(--ab-red)">' + fee + '</td>',
'<td style="color:var(--ab-orange);font-weight:700">' + net + '</td>',
'<td><span class="abd-status-badge ' + statusBadgeClass(t.status) + '">' + statusLabel(t.status) + '</span></td>',
].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 = '<p style="color:rgba(255,255,255,.5);font-size:14px;">Sin viajes programados próximamente.</p>';
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 = '<div class="abd-scheduled-time">' + escapeHtml(dt) + '</div>'
+ '<div class="abd-scheduled-meta">'
+ '👤 ' + escapeHtml(t.passenger_name || '—') + '<br>'
+ '📍 ' + escapeHtml(t.pickup_address || '—') + ' → ' + escapeHtml(t.dropoff_address || '—') + '<br>'
+ '💰 ' + (t.currency || '') + ' ' + parseFloat(t.fare_total_amount || t.estimated_fare || 0).toFixed(2)
+ '</div>';
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
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);
})();