feat: AutoBooking initial commit — PHP WordPress Plugin (REST API, wpdb, WP_User_Query)
This commit is contained in:
@@ -0,0 +1,675 @@
|
||||
/* 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);
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user