676 lines
38 KiB
JavaScript
676 lines
38 KiB
JavaScript
/* AutoBooking Admin Dashboard — admin-dashboard.js v1.0.0 */
|
|
(function () {
|
|
'use strict';
|
|
|
|
const CFG = window.AB_ADMIN_CFG || {};
|
|
const API = CFG.api_root + 'autobooking/v1/admin/';
|
|
const NONCE = CFG.nonce;
|
|
|
|
let lang = 'es';
|
|
let currentTab = 'overview';
|
|
let driversPage = 1, companiesPage = 1, tripsPage = 1, incidentsPage = 1;
|
|
let reasonCallback = null;
|
|
let currentIncidentId = null;
|
|
let zoneMap = null, zoneMarker = null, zoneCircle = null;
|
|
let tripMap = null, incidentMap = null, zonesOverviewMap = null;
|
|
|
|
/* ── API ── */
|
|
function apiFetch(path, opts) {
|
|
opts = opts || {};
|
|
return fetch(API + path, Object.assign({ headers: { 'X-WP-Nonce': NONCE, 'Content-Type': 'application/json' } }, opts)).then(function(r){ return r.json(); });
|
|
}
|
|
|
|
/* ── DOM ── */
|
|
function qs(sel, ctx) { return (ctx || document).querySelector(sel); }
|
|
function setText(id, val) { var el = qs('#' + id); if (el) el.textContent = val; }
|
|
function show(id) { var el = qs('#' + id); if (el) el.style.display = ''; }
|
|
function hide(id) { var el = qs('#' + id); if (el) el.style.display = 'none'; }
|
|
|
|
function applyLang() {
|
|
document.querySelectorAll('[data-es]').forEach(function(el) {
|
|
el.textContent = lang === 'en' ? (el.dataset.en || el.dataset.es) : el.dataset.es;
|
|
});
|
|
var btn = qs('#abad-lang-btn');
|
|
if (btn) btn.textContent = lang === 'en' ? 'EN' : 'ES';
|
|
}
|
|
|
|
function fmtMoney(v, currency) { return (currency || 'USD') + ' ' + parseFloat(v || 0).toFixed(2); }
|
|
function fmtDate(dt) {
|
|
if (!dt) return '—';
|
|
return new Date(dt).toLocaleString('es-CO', { dateStyle: 'short', timeStyle: 'short' });
|
|
}
|
|
|
|
function escHtml(str) {
|
|
if (str == null) return '';
|
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
function statusDot(online) {
|
|
return '<span class="abad-dot ' + (online ? 'abad-dot--green' : 'abad-dot--gray') + '"></span>';
|
|
}
|
|
|
|
function renderPagination(containerId, currentPage, total, perPage, onClick) {
|
|
var container = qs('#' + containerId);
|
|
if (!container) return;
|
|
var totalPages = Math.ceil(total / perPage) || 1;
|
|
container.innerHTML = '';
|
|
if (totalPages <= 1) return;
|
|
|
|
function mkBtn(label, page, active) {
|
|
var btn = document.createElement('button');
|
|
btn.className = 'abad-page-btn' + (active ? ' abad-page-btn--active' : '');
|
|
btn.textContent = label;
|
|
btn.disabled = (page < 1 || page > totalPages);
|
|
btn.onclick = function() { onClick(page); };
|
|
container.appendChild(btn);
|
|
}
|
|
|
|
mkBtn(lang === 'en' ? 'Prev' : 'Ant', currentPage - 1, false);
|
|
for (var i = 1; i <= totalPages; i++) {
|
|
if (totalPages > 7 && Math.abs(i - currentPage) > 2 && i !== 1 && i !== totalPages) continue;
|
|
mkBtn(i, i, i === currentPage);
|
|
}
|
|
mkBtn(lang === 'en' ? 'Next' : 'Sig', currentPage + 1, false);
|
|
}
|
|
|
|
function darkMapStyle() {
|
|
return [
|
|
{ elementType: 'geometry', stylers: [{ color: '#212121' }] },
|
|
{ elementType: 'labels.text.fill', stylers: [{ color: '#757575' }] },
|
|
{ featureType: 'road', elementType: 'geometry', stylers: [{ color: '#2c2c2c' }] },
|
|
{ featureType: 'water', elementType: 'geometry', stylers: [{ color: '#000000' }] },
|
|
];
|
|
}
|
|
|
|
function downloadCSV(csv, filename) {
|
|
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url; a.download = filename || 'export.csv';
|
|
document.body.appendChild(a); a.click();
|
|
document.body.removeChild(a); URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/* ── Tabs ── */
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.abad-tab').forEach(function(b) {
|
|
b.classList.toggle('abad-tab--active', b.dataset.tab === tab);
|
|
});
|
|
document.querySelectorAll('.abad-panel').forEach(function(p) { p.style.display = 'none'; });
|
|
var panel = qs('#panel-' + tab);
|
|
if (panel) panel.style.display = '';
|
|
loadTab(tab);
|
|
}
|
|
|
|
function loadTab(tab) {
|
|
if (tab === 'overview') loadOverview();
|
|
if (tab === 'drivers') { loadDrivers(); loadPending(); }
|
|
if (tab === 'companies') loadCompanies();
|
|
if (tab === 'trips') loadTrips();
|
|
if (tab === 'incidents') loadIncidents();
|
|
if (tab === 'zones') loadZones();
|
|
if (tab === 'settings') loadSettings();
|
|
}
|
|
|
|
/* ── Overview ── */
|
|
function loadOverview() {
|
|
apiFetch('overview').then(function(data) {
|
|
setText('kpi-trips-today', data.trips_today != null ? data.trips_today : '—');
|
|
setText('kpi-trips-week', data.trips_week != null ? data.trips_week : '—');
|
|
setText('kpi-active', data.active_trips != null ? data.active_trips : '—');
|
|
setText('kpi-online', data.drivers_online != null ? data.drivers_online : '—');
|
|
setText('kpi-revenue', '$' + parseFloat(data.revenue_today || 0).toFixed(2));
|
|
setText('kpi-sos', data.sos_open != null ? data.sos_open : 0);
|
|
setText('kpi-pending', data.pending_drivers != null ? data.pending_drivers : 0);
|
|
|
|
var sosWrap = qs('#kpi-sos-wrap');
|
|
var pendWrap = qs('#kpi-pending-wrap');
|
|
if (sosWrap) sosWrap.style.display = data.sos_open > 0 ? '' : 'none';
|
|
if (pendWrap) pendWrap.style.display = data.pending_drivers > 0 ? '' : 'none';
|
|
|
|
var sosInd = qs('#abad-sos-indicator');
|
|
if (sosInd) { sosInd.style.display = data.sos_open > 0 ? 'flex' : 'none'; setText('abad-sos-count', data.sos_open); }
|
|
|
|
var sosBadge = qs('#tab-sos-badge');
|
|
var pendBadge = qs('#tab-pending-badge');
|
|
if (sosBadge) { sosBadge.style.display = data.sos_open > 0 ? '' : 'none'; sosBadge.textContent = data.sos_open; }
|
|
if (pendBadge) { pendBadge.style.display = data.pending_drivers > 0 ? '' : 'none'; pendBadge.textContent = data.pending_drivers; }
|
|
|
|
var inline = qs('#pending-count-inline');
|
|
if (inline) inline.textContent = data.pending_drivers > 0 ? data.pending_drivers : '';
|
|
|
|
renderChart(data.chart || []);
|
|
});
|
|
}
|
|
|
|
function renderChart(chart) {
|
|
var container = qs('#abad-overview-chart');
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
if (!chart.length) { container.innerHTML = '<p class="abad-empty-state">Sin datos.</p>'; return; }
|
|
var max = Math.max.apply(null, chart.map(function(d){ return parseInt(d.total) || 0; })) || 1;
|
|
chart.forEach(function(day) {
|
|
var bar = document.createElement('div');
|
|
bar.className = 'abad-bar-chart__bar';
|
|
var pct = Math.max(4, Math.round((parseInt(day.total || 0) / max) * 100));
|
|
bar.style.height = pct + '%';
|
|
bar.title = day.the_date + ': ' + day.total + ' viajes | $' + parseFloat(day.revenue || 0).toFixed(2);
|
|
container.appendChild(bar);
|
|
});
|
|
}
|
|
|
|
/* ── Drivers ── */
|
|
function loadDrivers(page) {
|
|
if (page) driversPage = page;
|
|
var search = (qs('#driver-search') || {}).value || '';
|
|
var filter = (qs('#driver-filter') || {}).value || '';
|
|
apiFetch('drivers?page=' + driversPage + '&search=' + encodeURIComponent(search) + '&filter=' + filter).then(function(data) {
|
|
var tbody = qs('#drivers-tbody');
|
|
if (!tbody) return;
|
|
if (!data.drivers || !data.drivers.length) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="abad-empty-state">Sin conductores</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = data.drivers.map(function(d) {
|
|
return '<tr>' +
|
|
'<td>' + (d.photo_url ? '<img src="' + escHtml(d.photo_url) + '" style="width:30px;height:30px;border-radius:50%;object-fit:cover;vertical-align:middle;margin-right:8px;">' : '') +
|
|
escHtml(d.name) + '<br><span style="font-size:11px;color:var(--ab-muted)">' + escHtml(d.email) + '</span></td>' +
|
|
'<td>' + escHtml(d.vehicle_type || '—') + ' ' + escHtml(d.vehicle_plate || '') + '</td>' +
|
|
'<td>' + statusDot(d.online) + (d.online ? 'Online' : 'Offline') + '</td>' +
|
|
'<td>' + (d.avg_rating > 0 ? '★ ' + d.avg_rating : '—') + '</td>' +
|
|
'<td>' + d.trips_total + '</td>' +
|
|
'<td><button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadSuspendDriver(' + d.id + ',\'' + escHtml(d.name) + '\')">Suspender</button></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
applyLang();
|
|
renderPagination('drivers-pagination', driversPage, data.total, data.per_page, loadDrivers);
|
|
});
|
|
}
|
|
|
|
function loadPending() {
|
|
apiFetch('drivers/pending').then(function(data) {
|
|
var list = qs('#abad-pending-list');
|
|
var count = (data.drivers || []).length;
|
|
|
|
var badge = qs('#tab-pending-badge');
|
|
var inline = qs('#pending-count-inline');
|
|
if (badge) { badge.style.display = count > 0 ? '' : 'none'; badge.textContent = count; }
|
|
if (inline) { inline.textContent = count > 0 ? count : ''; }
|
|
|
|
if (!list) return;
|
|
if (!count) { list.innerHTML = '<p class="abad-empty-state">No hay conductores pendientes.</p>'; return; }
|
|
|
|
list.innerHTML = data.drivers.map(function(d) {
|
|
return '<div class="abad-item-card abad-item-card--yellow">' +
|
|
'<div class="abad-item-card__name">' + escHtml(d.name) + '</div>' +
|
|
'<div class="abad-item-card__meta">' + escHtml(d.email) + ' | ' + escHtml(d.phone || '—') + '<br>' +
|
|
'Vehiculo: ' + escHtml(d.vehicle_type || '—') + ' ' + escHtml(d.vehicle_plate || '') + '<br>' +
|
|
'Registro: ' + fmtDate(d.registered) + '<br>' +
|
|
'<span style="color:var(--ab-blue);font-size:11px">TODO: documentos (ver CHANGES.md)</span>' +
|
|
'</div>' +
|
|
'<div class="abad-item-card__actions">' +
|
|
'<button class="abad-btn abad-btn--brand abad-btn--sm" onclick="abadApproveDriver(' + d.id + ')">Aprobar</button>' +
|
|
'<button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadRejectDriver(' + d.id + ',\'' + escHtml(d.name) + '\')">Rechazar</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
});
|
|
}
|
|
|
|
window.abadApproveDriver = function(uid) {
|
|
if (!confirm(lang === 'en' ? 'Approve driver?' : '¿Aprobar conductor?')) return;
|
|
apiFetch('drivers/approve', { method: 'POST', body: JSON.stringify({ user_id: uid }) }).then(function(r) {
|
|
if (r.ok) { loadDrivers(); loadPending(); loadOverview(); } else alert(r.message || 'Error');
|
|
});
|
|
};
|
|
|
|
window.abadRejectDriver = function(uid, name) {
|
|
showReasonModal((lang === 'en' ? 'Reject: ' : 'Rechazar: ') + name, function(reason) {
|
|
apiFetch('drivers/reject', { method: 'POST', body: JSON.stringify({ user_id: uid, reason: reason }) }).then(function(r) {
|
|
if (r.ok) { loadDrivers(); loadPending(); loadOverview(); } else alert(r.message || 'Error');
|
|
});
|
|
});
|
|
};
|
|
|
|
window.abadSuspendDriver = function(uid, name) {
|
|
showReasonModal((lang === 'en' ? 'Suspend: ' : 'Suspender: ') + name, function(reason) {
|
|
apiFetch('drivers/suspend', { method: 'POST', body: JSON.stringify({ user_id: uid, reason: reason }) }).then(function(r) {
|
|
if (r.ok) loadDrivers(); else alert(r.message || 'Error');
|
|
});
|
|
});
|
|
};
|
|
|
|
/* ── Companies ── */
|
|
function loadCompanies(page) {
|
|
if (page) companiesPage = page;
|
|
var search = (qs('#company-search') || {}).value || '';
|
|
apiFetch('companies?page=' + companiesPage + '&search=' + encodeURIComponent(search)).then(function(data) {
|
|
var tbody = qs('#companies-tbody');
|
|
if (!tbody) return;
|
|
if (!data.companies || !data.companies.length) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="abad-empty-state">Sin empresas</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = data.companies.map(function(c) {
|
|
var isActive = c.active == 1;
|
|
return '<tr>' +
|
|
'<td>' + escHtml(c.name) + '<br><span style="font-size:11px;color:var(--ab-muted)">' + escHtml(c.email) + '</span></td>' +
|
|
'<td>' + escHtml(c.contact_name || '—') + '<br><span style="font-size:11px;color:var(--ab-muted)">' + escHtml(c.phone || '') + '</span></td>' +
|
|
'<td>' + (c.trips_count || 0) + '</td>' +
|
|
'<td><span class="abad-dot ' + (isActive ? 'abad-dot--green' : 'abad-dot--gray') + '"></span>' + (isActive ? 'Activa' : 'Inactiva') + '</td>' +
|
|
'<td>' +
|
|
(isActive
|
|
? '<button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadDeactivateCompany(' + c.id + ')">Desactivar</button>'
|
|
: '<button class="abad-btn abad-btn--brand abad-btn--sm" onclick="abadActivateCompany(' + c.id + ')">Activar</button>') +
|
|
' <button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadCompanyInvoices(' + c.id + ',\'' + escHtml(c.name) + '\')">Facturas</button>' +
|
|
'</td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
renderPagination('companies-pagination', companiesPage, data.total, data.per_page, loadCompanies);
|
|
});
|
|
}
|
|
|
|
window.abadActivateCompany = function(cid) {
|
|
apiFetch('companies/activate', { method: 'POST', body: JSON.stringify({ company_id: cid }) }).then(function(r) {
|
|
if (r.ok) loadCompanies(); else alert(r.message || 'Error');
|
|
});
|
|
};
|
|
|
|
window.abadDeactivateCompany = function(cid) {
|
|
if (!confirm(lang === 'en' ? 'Deactivate company?' : '¿Desactivar empresa?')) return;
|
|
apiFetch('companies/deactivate', { method: 'POST', body: JSON.stringify({ company_id: cid }) }).then(function(r) {
|
|
if (r.ok) loadCompanies(); else alert(r.message || 'Error');
|
|
});
|
|
};
|
|
|
|
window.abadCompanyInvoices = function(cid) {
|
|
apiFetch(cid + '/invoices').then(function(data) {
|
|
console.log('Facturas empresa ' + cid + ':', data.invoices);
|
|
alert(lang === 'en' ? 'Invoices logged to console (' + (data.invoices || []).length + ' records).' : 'Facturas en consola (' + (data.invoices || []).length + ' registros).');
|
|
});
|
|
};
|
|
|
|
/* ── Trips ── */
|
|
function loadTrips(page) {
|
|
if (page) tripsPage = page;
|
|
var search = (qs('#trip-search') || {}).value || '';
|
|
var from = (qs('#trip-date-from') || {}).value || '';
|
|
var to = (qs('#trip-date-to') || {}).value || '';
|
|
var status = (qs('#trip-status-filter') || {}).value || '';
|
|
var params = 'page=' + tripsPage + '&search=' + encodeURIComponent(search) + '&date_from=' + from + '&date_to=' + to + '&status=' + status;
|
|
|
|
apiFetch('trips?' + params).then(function(data) {
|
|
var tbody = qs('#trips-tbody');
|
|
if (!tbody) return;
|
|
if (!data.trips || !data.trips.length) { tbody.innerHTML = '<tr><td colspan="8" class="abad-empty-state">Sin viajes</td></tr>'; return; }
|
|
|
|
tbody.innerHTML = data.trips.map(function(t) {
|
|
return '<tr>' +
|
|
'<td style="white-space:nowrap">' + fmtDate(t.created_at) + '</td>' +
|
|
'<td style="font-family:monospace;font-size:11px">' + escHtml((t.trip_uuid || '').slice(0,10)) + '…</td>' +
|
|
'<td>' + escHtml(t.driver_name || '—') + '</td>' +
|
|
'<td>' + escHtml(t.passenger_name || '—') + '</td>' +
|
|
'<td>' + fmtMoney(t.fare_total_amount, t.currency) + '</td>' +
|
|
'<td><span style="font-size:11px;padding:2px 7px;border-radius:20px;background:rgba(255,255,255,.08)">' + escHtml(t.status) + '</span></td>' +
|
|
'<td>' + (t.driver_rating ? '★ ' + t.driver_rating : '—') + '</td>' +
|
|
'<td><button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadTripDetail(' + t.id + ')">Ver</button></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
|
|
renderPagination('trips-pagination', tripsPage, data.total, data.per_page, loadTrips);
|
|
});
|
|
}
|
|
|
|
window.abadTripDetail = function(tid) {
|
|
apiFetch('trips/' + tid).then(function(data) {
|
|
var trip = data.trip || {};
|
|
show('modal-trip');
|
|
var content = qs('#modal-trip-content');
|
|
if (content) {
|
|
content.innerHTML = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px;font-size:13px">' +
|
|
'<div><strong>Conductor</strong><br>' + escHtml(trip.driver_name || '—') + '</div>' +
|
|
'<div><strong>Pasajero</strong><br>' + escHtml(trip.passenger_name || '—') + '</div>' +
|
|
'<div><strong>Estado</strong><br>' + escHtml(trip.status || '—') + '</div>' +
|
|
'<div><strong>Total</strong><br>' + fmtMoney(trip.fare_total_amount, trip.currency) + '</div>' +
|
|
'<div><strong>Comision</strong><br>' + fmtMoney(trip.platform_fee_amount, trip.currency) + '</div>' +
|
|
'<div><strong>Neto conductor</strong><br>'+ fmtMoney(trip.driver_payout_amount, trip.currency) + '</div>' +
|
|
'<div><strong>Rating</strong><br>' + (trip.driver_rating ? '★ ' + trip.driver_rating : '—') + '</div>' +
|
|
'<div><strong>Fecha</strong><br>' + fmtDate(trip.created_at) + '</div>' +
|
|
'</div>' +
|
|
(data.chat && data.chat.length ? '<div style="margin-top:14px"><strong>Chat</strong><div style="max-height:100px;overflow-y:auto;margin-top:6px">' +
|
|
data.chat.map(function(m) { return '<div style="margin-bottom:4px;font-size:12px"><b>' + escHtml(m.sender) + '</b>: ' + escHtml(m.message) + '</div>'; }).join('') +
|
|
'</div></div>' : '');
|
|
}
|
|
setTimeout(function() {
|
|
var mapEl = qs('#modal-trip-map');
|
|
if (!mapEl || !window.google || !data.positions || !data.positions.length) return;
|
|
var mid = data.positions[Math.floor(data.positions.length / 2)];
|
|
tripMap = new google.maps.Map(mapEl, { zoom: 13, center: { lat: parseFloat(mid.lat), lng: parseFloat(mid.lng) }, styles: darkMapStyle() });
|
|
new google.maps.Polyline({ path: data.positions.map(function(p){ return { lat: parseFloat(p.lat), lng: parseFloat(p.lng) }; }), geodesic: true, strokeColor: '#FF6F00', strokeOpacity: 0.9, strokeWeight: 3, map: tripMap });
|
|
}, 150);
|
|
});
|
|
};
|
|
|
|
/* ── Incidents ── */
|
|
function loadIncidents(page) {
|
|
if (page) incidentsPage = page;
|
|
var filter = (qs('#incident-filter') || {}).value || 'active';
|
|
apiFetch('incidents?filter=' + filter + '&page=' + incidentsPage).then(function(data) {
|
|
var list = qs('#abad-incidents-list');
|
|
if (!list) return;
|
|
if (!data.incidents || !data.incidents.length) { list.innerHTML = '<p class="abad-empty-state">No hay incidentes.</p>'; return; }
|
|
|
|
list.innerHTML = data.incidents.map(function(inc) {
|
|
return '<div class="abad-item-card ' + (inc.status === 'active' ? 'abad-item-card--red' : '') + '">' +
|
|
'<div class="abad-item-card__name"><span class="abad-dot ' + (inc.status === 'active' ? 'abad-dot--red' : 'abad-dot--green') + '"></span>' +
|
|
escHtml(inc.driver_name || 'Conductor #' + inc.driver_id) + '</div>' +
|
|
'<div class="abad-item-card__meta">' + fmtDate(inc.incident_at) + ' | Estado: <strong>' + escHtml(inc.status) + '</strong>' +
|
|
(inc.has_audio ? ' | <span style="color:var(--ab-blue)">Audio</span>' : '') +
|
|
(inc.notes ? '<br>Nota: ' + escHtml(inc.notes) : '') + '</div>' +
|
|
'<div class="abad-item-card__actions">' +
|
|
'<button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadOpenIncident(' + inc.id + ',' + (inc.lat||0) + ',' + (inc.lng||0) + ')">Ver mapa</button>' +
|
|
(inc.status === 'active' ?
|
|
'<button class="abad-btn abad-btn--brand abad-btn--sm" onclick="abadResolveIncident(' + inc.id + ',\'attended\')">Atendido</button>' +
|
|
'<button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadResolveIncident(' + inc.id + ',\'resolved\')">Resuelto</button>' : '') +
|
|
'</div></div>';
|
|
}).join('');
|
|
|
|
renderPagination('incidents-pagination', incidentsPage, data.total, 20, loadIncidents);
|
|
});
|
|
}
|
|
|
|
window.abadOpenIncident = function(id, lat, lng) {
|
|
currentIncidentId = id;
|
|
show('modal-incident');
|
|
var mapEl = qs('#modal-incident-map');
|
|
if (mapEl && window.google && lat && lng) {
|
|
setTimeout(function() {
|
|
incidentMap = new google.maps.Map(mapEl, { zoom: 15, center: { lat: parseFloat(lat), lng: parseFloat(lng) }, styles: darkMapStyle() });
|
|
new google.maps.Marker({ position: { lat: parseFloat(lat), lng: parseFloat(lng) }, map: incidentMap, icon: { path: google.maps.SymbolPath.CIRCLE, scale: 10, fillColor: '#ef4444', fillOpacity: 1, strokeColor: '#fff', strokeWeight: 2 } });
|
|
}, 150);
|
|
}
|
|
};
|
|
|
|
window.abadResolveIncident = function(id, status) {
|
|
var note = (qs('#incident-note') || {}).value || '';
|
|
apiFetch('incidents/' + id + '/resolve', { method: 'POST', body: JSON.stringify({ status: status, note: note }) }).then(function(r) {
|
|
if (r.ok) { hide('modal-incident'); loadIncidents(); loadOverview(); } else alert(r.message || 'Error');
|
|
});
|
|
};
|
|
|
|
/* ── Zones ── */
|
|
function loadZones() {
|
|
apiFetch('zones').then(function(zones) {
|
|
var list = qs('#abad-zones-list');
|
|
if (list) {
|
|
list.innerHTML = zones.length ? zones.map(function(z) {
|
|
return '<div class="abad-item-card">' +
|
|
'<div class="abad-item-card__name"><span class="abad-dot ' + (z.active ? 'abad-dot--green' : 'abad-dot--gray') + '"></span>' +
|
|
escHtml(z.title) + ' <span style="font-size:11px;color:var(--ab-muted)">[' + escHtml(z.type) + ']</span></div>' +
|
|
'<div class="abad-item-card__meta">Radio: ' + z.radius_km + ' km | Bono: ' + (z.bonus_amount > 0 ? '$'+z.bonus_amount+' '+z.bonus_currency : 'ninguno') +
|
|
(z.expires_at ? '<br>Expira: ' + fmtDate(z.expires_at) : '') + '</div>' +
|
|
'<div class="abad-item-card__actions">' +
|
|
'<button class="abad-btn abad-btn--outline abad-btn--sm" onclick="abadEditZone(' + z.id + ')">Editar</button>' +
|
|
(z.active ? '<button class="abad-btn abad-btn--danger abad-btn--sm" onclick="abadDeactivateZone(' + z.id + ')">Desactivar</button>' : '') +
|
|
'</div></div>';
|
|
}).join('') : '<p class="abad-empty-state">No hay zonas configuradas.</p>';
|
|
}
|
|
initZonesMap(zones);
|
|
});
|
|
}
|
|
|
|
function initZonesMap(zones) {
|
|
var mapEl = qs('#abad-zones-map');
|
|
if (!mapEl || !window.google) return;
|
|
setTimeout(function() {
|
|
zonesOverviewMap = new google.maps.Map(mapEl, { zoom: 11, center: { lat: 4.7110, lng: -74.0721 }, styles: darkMapStyle() });
|
|
zones.filter(function(z){ return z.active && z.lat && z.lng; }).forEach(function(z) {
|
|
new google.maps.Circle({ center: { lat: parseFloat(z.lat), lng: parseFloat(z.lng) }, radius: parseFloat(z.radius_km)*1000, map: zonesOverviewMap, strokeColor: '#FF6F00', strokeOpacity: 0.7, strokeWeight: 2, fillColor: '#FF6F00', fillOpacity: 0.12 });
|
|
});
|
|
}, 200);
|
|
}
|
|
|
|
window.abadEditZone = function(id) {
|
|
apiFetch('zones').then(function(zones) {
|
|
var z = null;
|
|
for (var i = 0; i < zones.length; i++) { if (zones[i].id == id) { z = zones[i]; break; } }
|
|
if (!z) return;
|
|
qs('#zone-id').value = z.id;
|
|
qs('#zone-title').value = z.title;
|
|
qs('#zone-type').value = z.type;
|
|
qs('#zone-description').value = z.description || '';
|
|
qs('#zone-radius').value = z.radius_km;
|
|
qs('#zone-bonus').value = z.bonus_amount;
|
|
qs('#zone-countdown').value = z.countdown_seconds;
|
|
qs('#zone-expires').value = z.expires_at ? z.expires_at.replace(' ','T').slice(0,16) : '';
|
|
qs('#zone-lat').value = z.lat;
|
|
qs('#zone-lng').value = z.lng;
|
|
show('modal-zone');
|
|
initZoneModal(parseFloat(z.lat), parseFloat(z.lng), parseFloat(z.radius_km));
|
|
});
|
|
};
|
|
|
|
window.abadDeactivateZone = function(id) {
|
|
if (!confirm(lang === 'en' ? 'Deactivate zone?' : '¿Desactivar zona?')) return;
|
|
apiFetch('zones/' + id + '/deactivate', { method: 'POST' }).then(function(r) { if (r.ok) loadZones(); });
|
|
};
|
|
|
|
function initZoneModal(lat, lng, radiusKm) {
|
|
var mapEl = qs('#zone-modal-map');
|
|
if (!mapEl || !window.google) return;
|
|
setTimeout(function() {
|
|
var center = (lat && lng) ? { lat: lat, lng: lng } : { lat: 4.7110, lng: -74.0721 };
|
|
zoneMap = new google.maps.Map(mapEl, { zoom: 12, center: center, styles: darkMapStyle() });
|
|
if (lat && lng) {
|
|
zoneMarker = new google.maps.Marker({ position: center, map: zoneMap });
|
|
zoneCircle = new google.maps.Circle({ center: center, radius: (radiusKm||2)*1000, map: zoneMap, strokeColor: '#FF6F00', strokeOpacity: 0.8, strokeWeight: 2, fillColor: '#FF6F00', fillOpacity: 0.15 });
|
|
}
|
|
zoneMap.addListener('click', function(e) {
|
|
var pos = { lat: e.latLng.lat(), lng: e.latLng.lng() };
|
|
qs('#zone-lat').value = pos.lat;
|
|
qs('#zone-lng').value = pos.lng;
|
|
if (zoneMarker) zoneMarker.setMap(null);
|
|
if (zoneCircle) zoneCircle.setMap(null);
|
|
zoneMarker = new google.maps.Marker({ position: pos, map: zoneMap });
|
|
var r = parseFloat((qs('#zone-radius')||{}).value || 2) * 1000;
|
|
zoneCircle = new google.maps.Circle({ center: pos, radius: r, map: zoneMap, strokeColor: '#FF6F00', strokeOpacity: 0.8, strokeWeight: 2, fillColor: '#FF6F00', fillOpacity: 0.15 });
|
|
});
|
|
}, 200);
|
|
}
|
|
|
|
/* ── Settings ── */
|
|
function loadSettings() {
|
|
apiFetch('settings').then(function(s) {
|
|
if (qs('#cfg-name')) qs('#cfg-name').value = s.platform_name || '';
|
|
if (qs('#cfg-logo')) qs('#cfg-logo').value = s.platform_logo || '';
|
|
if (qs('#cfg-sos-email')) qs('#cfg-sos-email').value = s.sos_email || '';
|
|
if (qs('#cfg-fee-pct')) qs('#cfg-fee-pct').value = (parseFloat(s.platform_fee_pct||0.20)*100).toFixed(1);
|
|
});
|
|
loadFareConfig();
|
|
}
|
|
|
|
function loadFareConfig() {
|
|
apiFetch('fare-config').then(function(rows) {
|
|
var list = qs('#abad-fare-list');
|
|
if (!list) return;
|
|
if (!rows.length) { list.innerHTML = '<p class="abad-empty-state">Sin tarifas.</p>'; return; }
|
|
list.innerHTML = '<table class="abad-table" style="margin-top:8px"><thead><tr><th>Pais</th><th>Moneda</th><th>Base</th><th>km</th><th>min</th><th>Comision</th><th>Min.</th></tr></thead><tbody>' +
|
|
rows.map(function(r) {
|
|
return '<tr><td><strong>' + escHtml(r.country_code) + '</strong></td><td>' + escHtml(r.currency) + '</td>' +
|
|
'<td>$' + parseFloat(r.base_fare).toFixed(2) + '</td>' +
|
|
'<td>$' + parseFloat(r.per_km).toFixed(3) + '</td>' +
|
|
'<td>$' + parseFloat(r.per_minute).toFixed(3) + '</td>' +
|
|
'<td>' + (parseFloat(r.platform_fee_pct)*100).toFixed(1) + '%</td>' +
|
|
'<td>$' + parseFloat(r.minimum_fare).toFixed(2) + '</td></tr>';
|
|
}).join('') + '</tbody></table>';
|
|
});
|
|
}
|
|
|
|
/* ── Reason modal ── */
|
|
function showReasonModal(title, callback) {
|
|
reasonCallback = callback;
|
|
var el = qs('#modal-reason-title');
|
|
if (el) el.textContent = title;
|
|
var txt = qs('#modal-reason-text');
|
|
if (txt) txt.value = '';
|
|
show('modal-reason');
|
|
}
|
|
|
|
/* ── Init ── */
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
document.querySelectorAll('.abad-tab').forEach(function(btn) {
|
|
btn.addEventListener('click', function(){ switchTab(btn.dataset.tab); });
|
|
});
|
|
|
|
var langBtn = qs('#abad-lang-btn');
|
|
if (langBtn) langBtn.addEventListener('click', function() { lang = lang === 'es' ? 'en' : 'es'; applyLang(); });
|
|
|
|
var driverSearch = qs('#driver-search');
|
|
if (driverSearch) driverSearch.addEventListener('keydown', function(e){ if(e.key==='Enter'){ driversPage=1; loadDrivers(); } });
|
|
var driverFilter = qs('#driver-filter');
|
|
if (driverFilter) driverFilter.addEventListener('change', function(){ driversPage=1; loadDrivers(); });
|
|
|
|
var btnPending = qs('#btn-show-pending');
|
|
if (btnPending) btnPending.addEventListener('click', function() {
|
|
var sec = qs('#abad-pending-section');
|
|
if (sec) sec.style.display = sec.style.display === 'none' ? '' : 'none';
|
|
});
|
|
|
|
var companySearch = qs('#company-search');
|
|
if (companySearch) companySearch.addEventListener('keydown', function(e){ if(e.key==='Enter'){ companiesPage=1; loadCompanies(); } });
|
|
|
|
var btnTripSearch = qs('#btn-trip-search');
|
|
if (btnTripSearch) btnTripSearch.addEventListener('click', function(){ tripsPage=1; loadTrips(); });
|
|
|
|
var btnExportCSV = qs('#btn-export-trips-csv');
|
|
if (btnExportCSV) btnExportCSV.addEventListener('click', function() {
|
|
var from = (qs('#trip-date-from')||{}).value||'';
|
|
var to = (qs('#trip-date-to')||{}).value||'';
|
|
var status = (qs('#trip-status-filter')||{}).value||'';
|
|
apiFetch('trips/export-csv?date_from=' + from + '&date_to=' + to + '&status=' + status).then(function(r){ if(r.csv) downloadCSV(r.csv, r.filename||'viajes.csv'); });
|
|
});
|
|
|
|
var incidentFilter = qs('#incident-filter');
|
|
if (incidentFilter) incidentFilter.addEventListener('change', function(){ incidentsPage=1; loadIncidents(); });
|
|
var btnRefreshInc = qs('#btn-refresh-incidents');
|
|
if (btnRefreshInc) btnRefreshInc.addEventListener('click', loadIncidents);
|
|
|
|
var incClose = qs('#modal-incident-close');
|
|
if (incClose) incClose.addEventListener('click', function(){ hide('modal-incident'); });
|
|
|
|
var btnAttended = qs('#btn-incident-attended');
|
|
if (btnAttended) btnAttended.addEventListener('click', function(){ if(currentIncidentId) window.abadResolveIncident(currentIncidentId,'attended'); });
|
|
var btnResolved = qs('#btn-incident-resolved');
|
|
if (btnResolved) btnResolved.addEventListener('click', function(){ if(currentIncidentId) window.abadResolveIncident(currentIncidentId,'resolved'); });
|
|
|
|
var tripClose = qs('#modal-trip-close');
|
|
if (tripClose) tripClose.addEventListener('click', function(){ hide('modal-trip'); });
|
|
|
|
var reasonConfirm = qs('#modal-reason-confirm');
|
|
if (reasonConfirm) reasonConfirm.addEventListener('click', function() {
|
|
var reason = (qs('#modal-reason-text')||{}).value || '';
|
|
hide('modal-reason');
|
|
if (reasonCallback) { reasonCallback(reason); reasonCallback = null; }
|
|
});
|
|
var reasonCancel = qs('#modal-reason-cancel');
|
|
if (reasonCancel) reasonCancel.addEventListener('click', function(){ hide('modal-reason'); reasonCallback=null; });
|
|
|
|
var btnNewZone = qs('#btn-new-zone');
|
|
if (btnNewZone) btnNewZone.addEventListener('click', function() {
|
|
['#zone-id','#zone-title','#zone-description','#zone-expires','#zone-lat','#zone-lng'].forEach(function(s){ var el=qs(s); if(el) el.value=''; });
|
|
qs('#zone-radius').value=2; qs('#zone-bonus').value=0; qs('#zone-countdown').value=180;
|
|
show('modal-zone');
|
|
initZoneModal(0,0,2);
|
|
});
|
|
var zoneClose = qs('#modal-zone-close');
|
|
var zonCancel = qs('#btn-zone-cancel');
|
|
if (zoneClose) zoneClose.addEventListener('click', function(){ hide('modal-zone'); });
|
|
if (zonCancel) zonCancel.addEventListener('click', function(){ hide('modal-zone'); });
|
|
|
|
var btnZoneSave = qs('#btn-zone-save');
|
|
if (btnZoneSave) btnZoneSave.addEventListener('click', function() {
|
|
var lat = parseFloat((qs('#zone-lat')||{}).value||0);
|
|
var lng = parseFloat((qs('#zone-lng')||{}).value||0);
|
|
var title = (qs('#zone-title')||{}).value||'';
|
|
if (!title || !lat || !lng) { alert(lang==='en'?'Click the map and enter a title.':'Haz clic en el mapa y escribe un titulo.'); return; }
|
|
var expires = (qs('#zone-expires')||{}).value||'';
|
|
var payload = {
|
|
id: parseInt((qs('#zone-id')||{}).value||0),
|
|
title: title,
|
|
description: (qs('#zone-description')||{}).value||'',
|
|
type: (qs('#zone-type')||{}).value||'hotspot',
|
|
lat: lat, lng: lng,
|
|
radius_km: parseFloat((qs('#zone-radius')||{}).value||2),
|
|
bonus_amount: parseFloat((qs('#zone-bonus')||{}).value||0),
|
|
countdown_seconds: parseInt((qs('#zone-countdown')||{}).value||180),
|
|
expires_at: expires ? expires.replace('T',' ') : '',
|
|
};
|
|
apiFetch('zones/save', { method: 'POST', body: JSON.stringify(payload) }).then(function(r){ if(r.ok){ hide('modal-zone'); loadZones(); } else alert(r.message||'Error'); });
|
|
});
|
|
|
|
var btnSaveSettings = qs('#btn-save-settings');
|
|
if (btnSaveSettings) btnSaveSettings.addEventListener('click', function() {
|
|
var payload = {
|
|
platform_name: (qs('#cfg-name')||{}).value||'',
|
|
platform_logo: (qs('#cfg-logo')||{}).value||'',
|
|
sos_email: (qs('#cfg-sos-email')||{}).value||'',
|
|
platform_fee_pct: parseFloat((qs('#cfg-fee-pct')||{}).value||20)/100,
|
|
};
|
|
apiFetch('settings/save', { method:'POST', body: JSON.stringify(payload) }).then(function(r){
|
|
if(r.ok){ var el=qs('#cfg-success'); if(el){ el.style.display=''; setTimeout(function(){ el.style.display='none'; }, 2500); } }
|
|
});
|
|
});
|
|
|
|
var btnSaveFare = qs('#btn-save-fare');
|
|
if (btnSaveFare) btnSaveFare.addEventListener('click', function() {
|
|
var payload = {
|
|
country_code: ((qs('#fare-country')||{}).value||'').toUpperCase(),
|
|
currency: ((qs('#fare-currency')||{}).value||'USD').toUpperCase(),
|
|
base_fare: parseFloat((qs('#fare-base')||{}).value||3),
|
|
per_km: parseFloat((qs('#fare-per-km')||{}).value||1.80),
|
|
per_minute: parseFloat((qs('#fare-per-min')||{}).value||0.30),
|
|
minimum_fare: parseFloat((qs('#fare-minimum')||{}).value||5),
|
|
platform_fee_pct: parseFloat((qs('#cfg-fee-pct')||{}).value||20)/100,
|
|
};
|
|
apiFetch('fare-config/save', { method:'POST', body: JSON.stringify(payload) }).then(function(r){
|
|
if(r.ok){ loadFareConfig(); var el=qs('#fare-success'); if(el){ el.style.display=''; setTimeout(function(){ el.style.display='none'; }, 2500); } }
|
|
else alert(r.message||'Error');
|
|
});
|
|
});
|
|
|
|
applyLang();
|
|
switchTab('overview');
|
|
setInterval(function(){ if(currentTab==='overview') loadOverview(); }, 60000);
|
|
});
|
|
|
|
// ── SECURITY TAB ──────────────────────────────────────────
|
|
async function loadSecuritySettings() {
|
|
try {
|
|
const r = await apiFetch('blocked-ips');
|
|
const countEl = document.getElementById('sec-blocked-count');
|
|
if (countEl) countEl.textContent = r.blocked_24h + ' IPs bloqueadas en las últimas 24h';
|
|
const el = document.getElementById('sec-blocked-table');
|
|
if (!el) return;
|
|
if (!r.blocked.length) { el.innerHTML = '<p style="color:rgba(255,255,255,.4);font-size:13px">Sin IPs bloqueadas.</p>'; return; }
|
|
el.innerHTML = '<table class="abad-table"><thead><tr><th>IP</th><th>Motivo</th><th>Expira</th><th></th></tr></thead><tbody>'
|
|
+ r.blocked.map(b => '<tr><td>'+b.ip+'</td><td style="font-size:12px">'+b.reason+'</td><td style="font-size:12px">'+b.expires_at+'</td><td><button class="abad-btn abad-btn--outline abad-btn--sm" onclick="absUnblock('+b.id+')">Desbloquear</button></td></tr>').join('')
|
|
+ '</tbody></table>';
|
|
} catch(e) {}
|
|
}
|
|
window.absUnblock = async function(id) { await apiFetch('blocked-ips/'+id+'/unblock',{method:'POST'}); loadSecuritySettings(); };
|
|
document.getElementById('btn-save-sec') && document.getElementById('btn-save-sec').addEventListener('click', async function() {
|
|
await apiFetch('security-settings/save', {method:'POST', body: JSON.stringify({
|
|
whitelist: document.getElementById('sec-whitelist').value,
|
|
hcaptcha_site_key: document.getElementById('sec-hcaptcha-site').value,
|
|
hcaptcha_secret: document.getElementById('sec-hcaptcha-secret').value,
|
|
})});
|
|
var el = document.getElementById('sec-saved'); el.style.display='block'; setTimeout(function(){el.style.display='none';},3000);
|
|
});
|
|
document.querySelectorAll('.abad-tab').forEach(function(btn) {
|
|
if (btn.dataset.tab === 'settings') btn.addEventListener('click', loadSecuritySettings);
|
|
});
|
|
|
|
})();
|