A2 — Virtual scroll en tablas grandes: - Nuevo helper VirtualScroll en pos/static/js/virtual-scroll.js - inventory.js: tabla de productos con virtual scroll - customers.js: tabla de clientes con virtual scroll - fleet.js: renderMaintenance() y renderHistory() con virtual scroll - Templates envueltos en .vs-container para scroll A3 — Celery worker queue: - pos/celery_app.py + pos/tasks.py (warm cache, bulk import, reports) - Blueprint tasks_bp.py con endpoints /pos/api/tasks/* - Script scripts/start_celery.sh A4 — asyncpg + Quart PoC: - pos/async_catalog.py: endpoint /pos/api/catalog/async-search - scripts/benchmark_async_catalog.py: benchmark Flask vs Quart A5 — Particionar vehicle_parts: - scripts/partition_vehicle_parts.py: migración segura por hash (16 particiones) - Soporta --dry-run, --skip-swap, --skip-drop Tests: 36/36 pasando
635 lines
25 KiB
JavaScript
635 lines
25 KiB
JavaScript
/**
|
|
* 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 =
|
|
'<div class="empty-state"><div class="empty-state__text">Error al cargar vehiculos</div></div>';
|
|
});
|
|
}
|
|
|
|
function renderVehicleGrid(list) {
|
|
var grid = document.getElementById('vehicleGrid');
|
|
if (!list.length) {
|
|
grid.innerHTML = '<div class="empty-state">' +
|
|
'<div class="empty-state__icon">🚚</div>' +
|
|
'<div class="empty-state__text">No hay vehiculos registrados</div>' +
|
|
'<button class="btn btn--primary" onclick="Fleet.openVehicleModal()">+ Agregar Vehiculo</button>' +
|
|
'</div>';
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
list.forEach(function(v) {
|
|
var label = (v.make || '') + ' ' + (v.model || '');
|
|
if (v.year) label += ' ' + v.year;
|
|
|
|
html += '<div class="vehicle-card" onclick="Fleet.viewVehicle(' + v.id + ')">' +
|
|
'<div class="vehicle-card__header">' +
|
|
'<span class="vehicle-card__plate">' + esc(v.plate || 'SIN PLACA') + '</span>' +
|
|
'<span class="badge ' + (v.is_active ? 'badge--active' : 'badge--inactive') + '">' +
|
|
(v.is_active ? 'Activo' : 'Inactivo') + '</span>' +
|
|
'</div>' +
|
|
'<div class="vehicle-card__info">' +
|
|
'<div><strong>' + esc(label.trim()) + '</strong></div>' +
|
|
(v.owner_name ? '<div>' + esc(v.owner_name) + '</div>' : '') +
|
|
(v.color ? '<div>Color: ' + esc(v.color) + '</div>' : '') +
|
|
'</div>' +
|
|
'<div class="vehicle-card__footer">' +
|
|
'<span>' + fmt(v.current_mileage) + ' km</span>' +
|
|
'<span>' + esc(v.fuel_type || '') + '</span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
function renderPagination(p) {
|
|
var el = document.getElementById('vehiclePagination');
|
|
if (!p || p.total_pages <= 1) { el.innerHTML = ''; return; }
|
|
|
|
el.innerHTML = '<button class="pagination__btn" ' + (p.page <= 1 ? 'disabled' : '') +
|
|
' onclick="Fleet.goPage(' + (p.page - 1) + ')">« Anterior</button>' +
|
|
'<span class="pagination__info">Pagina ' + p.page + ' de ' + p.total_pages +
|
|
' (' + p.total + ' vehiculos)</span>' +
|
|
'<button class="pagination__btn" ' + (p.page >= p.total_pages ? 'disabled' : '') +
|
|
' onclick="Fleet.goPage(' + (p.page + 1) + ')">Siguiente »</button>';
|
|
}
|
|
|
|
function goPage(n) {
|
|
currentPage = n;
|
|
loadVehicles();
|
|
}
|
|
|
|
function populateVehicleSelects(list) {
|
|
var opts = '<option value="">-- Seleccionar vehiculo --</option>';
|
|
list.forEach(function(v) {
|
|
var label = (v.plate || 'S/P') + ' - ' + (v.make || '') + ' ' + (v.model || '');
|
|
opts += '<option value="' + v.id + '">' + esc(label.trim()) + '</option>';
|
|
});
|
|
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 =
|
|
'<tr><td colspan="7" style="text-align:center;color:var(--color-text-muted);">Error al cargar</td></tr>';
|
|
});
|
|
}
|
|
|
|
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 '<tr>' +
|
|
'<td><strong>' + esc(v.plate || 'S/P') + '</strong><br>' +
|
|
'<span style="font-size:var(--text-caption);color:var(--color-text-muted);">' +
|
|
esc((v.make || '') + ' ' + (v.model || '')) + '</span></td>' +
|
|
'<td>' + esc(s.maintenance_type) + '</td>' +
|
|
'<td class="mono">' + (interval || '—') + '</td>' +
|
|
'<td>' + fmtDate(s.last_done_at) + (s.last_done_km ? '<br><span class="mono" style="font-size:var(--text-caption);">' + fmt(s.last_done_km) + ' km</span>' : '') + '</td>' +
|
|
'<td>' + (next || '—') + '</td>' +
|
|
'<td><span class="badge ' + (isOverdue ? 'badge--overdue' : 'badge--active') + '">' +
|
|
(isOverdue ? 'Vencido' : 'Al dia') + '</span></td>' +
|
|
'<td><button class="btn btn--sm btn--ghost" onclick="Fleet.openLogModalFor(' + v.id + ',' + s.id + ',\'' + esc(s.maintenance_type) + '\')">Registrar</button></td>' +
|
|
'</tr>';
|
|
}
|
|
|
|
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 = '<tr><td colspan="7" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">' +
|
|
'No hay programas de mantenimiento.<br><button class="btn btn--primary btn--sm" style="margin-top:var(--space-3);" onclick="Fleet.openScheduleModal()">+ Crear Programa</button></td></tr>';
|
|
return;
|
|
}
|
|
|
|
if (!maintVS) {
|
|
maintVS = new VirtualScroll({
|
|
container: body,
|
|
rowHeight: 64,
|
|
buffer: 3,
|
|
renderRow: renderMaintRow,
|
|
emptyHtml: '<tr><td colspan="7" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay programas de mantenimiento.</td></tr>'
|
|
});
|
|
}
|
|
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 =
|
|
'<tr><td colspan="7" style="text-align:center;color:var(--color-text-muted);">Error al cargar</td></tr>';
|
|
});
|
|
}
|
|
|
|
function renderHistoryRow(l) {
|
|
return '<tr>' +
|
|
'<td>' + fmtDate(l.created_at) + '</td>' +
|
|
'<td><strong>' + esc(l._plate) + '</strong><br>' +
|
|
'<span style="font-size:var(--text-caption);color:var(--color-text-muted);">' + esc(l._make) + '</span></td>' +
|
|
'<td>' + esc(l.maintenance_type) + '</td>' +
|
|
'<td class="mono">' + fmt(l.mileage_at) + '</td>' +
|
|
'<td class="mono">' + fmtMoney(l.cost) + '</td>' +
|
|
'<td>' + esc(l.employee_name || '—') + '</td>' +
|
|
'<td style="max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + esc(l.notes || '—') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
|
|
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 = '<tr><td colspan="7" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay registros de mantenimiento</td></tr>';
|
|
return;
|
|
}
|
|
|
|
if (!historyVS) {
|
|
historyVS = new VirtualScroll({
|
|
container: body,
|
|
rowHeight: 48,
|
|
buffer: 3,
|
|
renderRow: renderHistoryRow,
|
|
emptyHtml: '<tr><td colspan="7" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay registros de mantenimiento</td></tr>'
|
|
});
|
|
}
|
|
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 =
|
|
'<div class="empty-state"><div class="empty-state__text">Error al cargar alertas</div></div>';
|
|
});
|
|
}
|
|
|
|
function renderAlerts(list) {
|
|
var el = document.getElementById('alertsList');
|
|
if (!list.length) {
|
|
el.innerHTML = '<div class="empty-state">' +
|
|
'<div class="empty-state__icon" style="color:#3FB950;">✓</div>' +
|
|
'<div class="empty-state__text">No hay mantenimientos vencidos</div>' +
|
|
'</div>';
|
|
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 += '<div class="alert-card">' +
|
|
'<div class="alert-card__info">' +
|
|
'<div class="alert-card__vehicle">' +
|
|
esc(a.plate || 'S/P') + ' — ' + esc((a.make || '') + ' ' + (a.model || '')) +
|
|
(a.year ? ' ' + a.year : '') +
|
|
'</div>' +
|
|
'<div class="alert-card__detail">' +
|
|
'<span class="badge badge--overdue">' + esc(a.maintenance_type) + '</span>' +
|
|
' ' + detail +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<button class="btn btn--sm btn--primary" onclick="Fleet.openLogModalFor(' + a.vehicle_id + ',' + a.schedule_id + ',\'' + esc(a.maintenance_type) + '\')">Registrar Mant.</button>' +
|
|
'</div>';
|
|
});
|
|
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 = '<option value="">-- Sin programa --</option>';
|
|
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 = '<option value="">-- Sin programa --</option>';
|
|
(d.data || []).forEach(function(s) {
|
|
var sel = (s.id == scheduleId) ? ' selected' : '';
|
|
opts += '<option value="' + s.id + '"' + sel + '>' + esc(s.maintenance_type) + '</option>';
|
|
});
|
|
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,
|
|
};
|
|
|
|
})();
|