/**
* fleet.js — Fleet Management module for Nexus POS
*
* Handles vehicle CRUD, maintenance schedules, logs, and alerts.
*/
var Fleet = (function() {
'use strict';
var API = '/pos/api/fleet';
var token = localStorage.getItem('pos_token');
var vehicles = [];
var currentPage = 1;
var searchTimeout = null;
// ─── Helpers ───
function headers() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
};
}
function fmt(n) {
if (n == null) return '0';
return parseFloat(n).toLocaleString('es-MX');
}
function fmtMoney(n) {
if (n == null) return '$0.00';
return '$' + parseFloat(n).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function fmtDate(d) {
if (!d) return '—';
var dt = new Date(d);
return dt.toLocaleDateString('es-MX', {day: '2-digit', month: 'short', year: 'numeric'});
}
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ─── Init ───
function init() {
// Tab switching
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var tab = this.getAttribute('data-tab');
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('is-active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
this.classList.add('is-active');
document.getElementById('tab-' + tab).classList.add('is-active');
if (tab === 'maintenance') loadMaintenance();
if (tab === 'history') loadHistory();
if (tab === 'alerts') loadAlerts();
});
});
// Search
var searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
currentPage = 1;
loadVehicles();
}, 300);
});
}
// New vehicle button
document.getElementById('btnNewVehicle').addEventListener('click', function() {
openVehicleModal();
});
// Load initial data
loadStats();
loadVehicles();
}
// ─── Stats ───
function loadStats() {
fetch(API + '/stats', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
document.getElementById('statTotal').textContent = fmt(d.total_vehicles);
document.getElementById('statOverdue').textContent = fmt(d.overdue_count);
document.getElementById('statUpcoming').textContent = fmt(d.upcoming_this_month);
document.getElementById('statCost').textContent = fmtMoney(d.total_cost_month);
})
.catch(function() {});
}
// ─── Vehicles Tab ───
function loadVehicles() {
var q = (document.getElementById('searchInput') || {}).value || '';
var url = API + '/vehicles?page=' + currentPage + '&per_page=50';
if (q) url += '&q=' + encodeURIComponent(q);
fetch(url, {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
vehicles = d.data || [];
renderVehicleGrid(vehicles);
renderPagination(d.pagination);
populateVehicleSelects(vehicles);
})
.catch(function() {
document.getElementById('vehicleGrid').innerHTML =
'
Error al cargar vehiculos
';
});
}
function renderVehicleGrid(list) {
var grid = document.getElementById('vehicleGrid');
if (!list.length) {
grid.innerHTML = '' +
'
🚚
' +
'
No hay vehiculos registrados
' +
'
+ Agregar Vehiculo ' +
'
';
return;
}
var html = '';
list.forEach(function(v) {
var label = (v.make || '') + ' ' + (v.model || '');
if (v.year) label += ' ' + v.year;
html += '' +
'' +
'
' +
'
' + esc(label.trim()) + '
' +
(v.owner_name ? '
' + esc(v.owner_name) + '
' : '') +
(v.color ? '
Color: ' + esc(v.color) + '
' : '') +
'
' +
'' +
'
';
});
grid.innerHTML = html;
}
function renderPagination(p) {
var el = document.getElementById('vehiclePagination');
if (!p || p.total_pages <= 1) { el.innerHTML = ''; return; }
el.innerHTML = '' +
'' +
'';
}
function goPage(n) {
currentPage = n;
loadVehicles();
}
function populateVehicleSelects(list) {
var opts = '-- Seleccionar vehiculo -- ';
list.forEach(function(v) {
var label = (v.plate || 'S/P') + ' - ' + (v.make || '') + ' ' + (v.model || '');
opts += '' + esc(label.trim()) + ' ';
});
var selects = ['schedVehicleSelect', 'logVehicleSelect'];
selects.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.innerHTML = opts;
});
}
// ─── Vehicle Detail (view in edit modal) ───
function viewVehicle(id) {
fetch(API + '/vehicles/' + id, {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(v) {
openVehicleModal(v);
});
}
// ─── Vehicle Modal ───
function openVehicleModal(data) {
var modal = document.getElementById('vehicleModal');
var title = document.getElementById('vehicleModalTitle');
if (data && data.id) {
title.textContent = 'Editar Vehiculo';
document.getElementById('vehEditId').value = data.id;
document.getElementById('vehPlate').value = data.plate || '';
document.getElementById('vehVin').value = data.vin || '';
document.getElementById('vehMake').value = data.make || '';
document.getElementById('vehModel').value = data.model || '';
document.getElementById('vehYear').value = data.year || '';
document.getElementById('vehMileage').value = data.current_mileage || 0;
document.getElementById('vehFuel').value = data.fuel_type || 'gasolina';
document.getElementById('vehColor').value = data.color || '';
document.getElementById('vehOwner').value = data.owner_name || '';
document.getElementById('vehNotes').value = data.notes || '';
} else {
title.textContent = 'Nuevo Vehiculo';
document.getElementById('vehEditId').value = '';
['vehPlate','vehVin','vehMake','vehModel','vehYear','vehColor','vehOwner','vehNotes']
.forEach(function(id) { document.getElementById(id).value = ''; });
document.getElementById('vehMileage').value = '0';
document.getElementById('vehFuel').value = 'gasolina';
}
modal.classList.add('is-open');
}
function closeVehicleModal() {
document.getElementById('vehicleModal').classList.remove('is-open');
}
function saveVehicle() {
var editId = document.getElementById('vehEditId').value;
var payload = {
plate: document.getElementById('vehPlate').value.trim(),
vin: document.getElementById('vehVin').value.trim(),
make: document.getElementById('vehMake').value.trim(),
model: document.getElementById('vehModel').value.trim(),
year: parseInt(document.getElementById('vehYear').value) || null,
current_mileage: parseInt(document.getElementById('vehMileage').value) || 0,
fuel_type: document.getElementById('vehFuel').value,
color: document.getElementById('vehColor').value.trim(),
owner_name: document.getElementById('vehOwner').value.trim(),
notes: document.getElementById('vehNotes').value.trim(),
};
if (!payload.plate && !payload.vin) {
alert('Se requiere placa o VIN');
return;
}
var url = API + '/vehicles';
var method = 'POST';
if (editId) {
url += '/' + editId;
method = 'PUT';
}
fetch(url, {
method: method,
headers: headers(),
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error) { alert(d.error); return; }
closeVehicleModal();
loadVehicles();
loadStats();
})
.catch(function(e) { alert('Error: ' + e.message); });
}
// ─── Maintenance Tab ───
function loadMaintenance() {
// Load all vehicles with their schedules
fetch(API + '/vehicles?per_page=200', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
var allVehicles = d.data || [];
var promises = allVehicles.map(function(v) {
return fetch(API + '/vehicles/' + v.id + '/schedules', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(s) {
return {vehicle: v, schedules: s.data || []};
});
});
return Promise.all(promises);
})
.then(function(results) {
renderMaintenance(results);
})
.catch(function() {
document.getElementById('maintBody').innerHTML =
'Error al cargar ';
});
}
function renderMaintRow(item) {
var v = item.vehicle;
var s = item.schedule;
var now = new Date();
var isOverdue = false;
if (s.next_due_at && new Date(s.next_due_at) < now) isOverdue = true;
if (s.next_due_km && s.next_due_km <= v.current_mileage) isOverdue = true;
var interval = '';
if (s.interval_km) interval += fmt(s.interval_km) + ' km';
if (s.interval_km && s.interval_months) interval += ' / ';
if (s.interval_months) interval += s.interval_months + ' meses';
var next = '';
if (s.next_due_at) next += fmtDate(s.next_due_at);
if (s.next_due_at && s.next_due_km) next += ' / ';
if (s.next_due_km) next += fmt(s.next_due_km) + ' km';
return '' +
'' + esc(v.plate || 'S/P') + ' ' +
'' +
esc((v.make || '') + ' ' + (v.model || '')) + ' ' +
'' + esc(s.maintenance_type) + ' ' +
'' + (interval || '—') + ' ' +
'' + fmtDate(s.last_done_at) + (s.last_done_km ? '' + fmt(s.last_done_km) + ' km ' : '') + ' ' +
'' + (next || '—') + ' ' +
'' +
(isOverdue ? 'Vencido' : 'Al dia') + ' ' +
'Registrar ' +
' ';
}
var maintVS = null;
function renderMaintenance(results) {
var body = document.getElementById('maintBody');
var items = [];
results.forEach(function(r) {
r.schedules.forEach(function(s) {
items.push({vehicle: r.vehicle, schedule: s});
});
});
if (!items.length) {
body.innerHTML = '' +
'No hay programas de mantenimiento.+ Crear Programa ';
return;
}
if (!maintVS) {
maintVS = new VirtualScroll({
container: body,
rowHeight: 64,
buffer: 3,
renderRow: renderMaintRow,
emptyHtml: 'No hay programas de mantenimiento. '
});
}
maintVS.setData(items);
}
// ─── History Tab ───
function loadHistory() {
fetch(API + '/vehicles?per_page=200', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
var allVehicles = d.data || [];
var promises = allVehicles.map(function(v) {
return fetch(API + '/vehicles/' + v.id, {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(detail) {
return {vehicle: v, logs: detail.recent_logs || []};
});
});
return Promise.all(promises);
})
.then(function(results) {
renderHistory(results);
})
.catch(function() {
document.getElementById('historyBody').innerHTML =
'Error al cargar ';
});
}
function renderHistoryRow(l) {
return '' +
'' + fmtDate(l.created_at) + ' ' +
'' + esc(l._plate) + ' ' +
'' + esc(l._make) + ' ' +
'' + esc(l.maintenance_type) + ' ' +
'' + fmt(l.mileage_at) + ' ' +
'' + fmtMoney(l.cost) + ' ' +
'' + esc(l.employee_name || '—') + ' ' +
'' + esc(l.notes || '—') + ' ' +
' ';
}
var historyVS = null;
function renderHistory(results) {
var body = document.getElementById('historyBody');
var allLogs = [];
results.forEach(function(r) {
r.logs.forEach(function(l) {
l._plate = r.vehicle.plate || 'S/P';
l._make = (r.vehicle.make || '') + ' ' + (r.vehicle.model || '');
allLogs.push(l);
});
});
// Sort by date desc
allLogs.sort(function(a, b) {
return new Date(b.created_at) - new Date(a.created_at);
});
if (!allLogs.length) {
body.innerHTML = 'No hay registros de mantenimiento ';
return;
}
if (!historyVS) {
historyVS = new VirtualScroll({
container: body,
rowHeight: 48,
buffer: 3,
renderRow: renderHistoryRow,
emptyHtml: 'No hay registros de mantenimiento '
});
}
historyVS.setData(allLogs);
}
// ─── Alerts Tab ───
function loadAlerts() {
fetch(API + '/alerts', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
renderAlerts(d.data || []);
})
.catch(function() {
document.getElementById('alertsList').innerHTML =
'';
});
}
function renderAlerts(list) {
var el = document.getElementById('alertsList');
if (!list.length) {
el.innerHTML = '' +
'
✓
' +
'
No hay mantenimientos vencidos
' +
'
';
return;
}
var html = '';
list.forEach(function(a) {
var detail = '';
if (a.next_due_at) detail += 'Vencio: ' + fmtDate(a.next_due_at);
if (a.next_due_km) detail += (detail ? ' | ' : '') + 'Limite: ' + fmt(a.next_due_km) + ' km (actual: ' + fmt(a.current_mileage) + ' km)';
html += '' +
'
' +
'
' +
esc(a.plate || 'S/P') + ' — ' + esc((a.make || '') + ' ' + (a.model || '')) +
(a.year ? ' ' + a.year : '') +
'
' +
'
' +
'' + esc(a.maintenance_type) + ' ' +
' ' + detail +
'
' +
'
' +
'
Registrar Mant. ' +
'
';
});
el.innerHTML = html;
}
// ─── Schedule Modal ───
function openScheduleModal(vehicleId) {
var modal = document.getElementById('scheduleModal');
document.getElementById('schedType').value = '';
document.getElementById('schedIntervalKm').value = '';
document.getElementById('schedIntervalMonths').value = '';
document.getElementById('schedNextDate').value = '';
document.getElementById('schedNextKm').value = '';
document.getElementById('schedNotes').value = '';
if (vehicleId) {
document.getElementById('schedVehicleSelect').value = vehicleId;
}
modal.classList.add('is-open');
}
function closeScheduleModal() {
document.getElementById('scheduleModal').classList.remove('is-open');
}
function saveSchedule() {
var vehicleId = document.getElementById('schedVehicleSelect').value;
if (!vehicleId) { alert('Seleccione un vehiculo'); return; }
var mType = document.getElementById('schedType').value.trim();
if (!mType) { alert('Ingrese el tipo de mantenimiento'); return; }
var payload = {
maintenance_type: mType,
interval_km: parseInt(document.getElementById('schedIntervalKm').value) || null,
interval_months: parseInt(document.getElementById('schedIntervalMonths').value) || null,
next_due_at: document.getElementById('schedNextDate').value || null,
next_due_km: parseInt(document.getElementById('schedNextKm').value) || null,
notes: document.getElementById('schedNotes').value.trim(),
};
fetch(API + '/vehicles/' + vehicleId + '/schedules', {
method: 'POST',
headers: headers(),
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error) { alert(d.error); return; }
closeScheduleModal();
loadMaintenance();
loadStats();
})
.catch(function(e) { alert('Error: ' + e.message); });
}
// ─── Log Modal ───
function openLogModal() {
var modal = document.getElementById('logModal');
document.getElementById('logScheduleSelect').innerHTML = '-- Sin programa -- ';
document.getElementById('logType').value = '';
document.getElementById('logMileage').value = '';
document.getElementById('logCost').value = '';
document.getElementById('logParts').value = '';
document.getElementById('logNotes').value = '';
modal.classList.add('is-open');
}
function openLogModalFor(vehicleId, scheduleId, maintType) {
openLogModal();
document.getElementById('logVehicleSelect').value = vehicleId;
if (maintType) document.getElementById('logType').value = maintType;
// Load schedules for this vehicle
fetch(API + '/vehicles/' + vehicleId + '/schedules', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
var opts = '-- Sin programa -- ';
(d.data || []).forEach(function(s) {
var sel = (s.id == scheduleId) ? ' selected' : '';
opts += '' + esc(s.maintenance_type) + ' ';
});
document.getElementById('logScheduleSelect').innerHTML = opts;
});
// Pre-fill mileage
var veh = vehicles.find(function(v) { return v.id == vehicleId; });
if (veh) document.getElementById('logMileage').value = veh.current_mileage || '';
}
function closeLogModal() {
document.getElementById('logModal').classList.remove('is-open');
}
function saveLog() {
var vehicleId = document.getElementById('logVehicleSelect').value;
if (!vehicleId) { alert('Seleccione un vehiculo'); return; }
var mType = document.getElementById('logType').value.trim();
if (!mType) { alert('Ingrese el tipo de mantenimiento'); return; }
var payload = {
schedule_id: parseInt(document.getElementById('logScheduleSelect').value) || null,
maintenance_type: mType,
mileage_at: parseInt(document.getElementById('logMileage').value) || null,
cost: parseFloat(document.getElementById('logCost').value) || 0,
parts_used: document.getElementById('logParts').value.trim(),
notes: document.getElementById('logNotes').value.trim(),
};
fetch(API + '/vehicles/' + vehicleId + '/log', {
method: 'POST',
headers: headers(),
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error) { alert(d.error); return; }
closeLogModal();
loadStats();
// Refresh current tab
var activeTab = document.querySelector('.tab-btn.is-active');
if (activeTab) {
var tab = activeTab.getAttribute('data-tab');
if (tab === 'maintenance') loadMaintenance();
if (tab === 'history') loadHistory();
if (tab === 'alerts') loadAlerts();
if (tab === 'vehicles') loadVehicles();
}
})
.catch(function(e) { alert('Error: ' + e.message); });
}
// ─── Public API ───
document.addEventListener('DOMContentLoaded', init);
return {
openVehicleModal: openVehicleModal,
closeVehicleModal: closeVehicleModal,
saveVehicle: saveVehicle,
viewVehicle: viewVehicle,
goPage: goPage,
openScheduleModal: openScheduleModal,
closeScheduleModal: closeScheduleModal,
saveSchedule: saveSchedule,
openLogModal: openLogModal,
openLogModalFor: openLogModalFor,
closeLogModal: closeLogModal,
saveLog: saveLog,
};
})();