/** * 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
' + '' + '
'; return; } var html = ''; list.forEach(function(v) { var label = (v.make || '') + ' ' + (v.model || ''); if (v.year) label += ' ' + v.year; html += '
' + '
' + '' + esc(v.plate || 'SIN PLACA') + '' + '' + (v.is_active ? 'Activo' : 'Inactivo') + '' + '
' + '
' + '
' + 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 = '' + 'Pagina ' + p.page + ' de ' + p.total_pages + ' (' + p.total + ' vehiculos)' + ''; } function goPage(n) { currentPage = n; loadVehicles(); } function populateVehicleSelects(list) { var opts = ''; list.forEach(function(v) { var label = (v.plate || 'S/P') + ' - ' + (v.make || '') + ' ' + (v.model || ''); opts += ''; }); 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') + '' + '' + ''; } 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.
'; 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 = '
Error al cargar alertas
'; }); } 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 + '
' + '
' + '' + '
'; }); 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 = ''; 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 = ''; (d.data || []).forEach(function(s) { var sel = (s.id == scheduleId) ? ' selected' : ''; opts += ''; }); 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, }; })();