OPCIÓN A: A2 Virtual Scroll + A3 Celery + A4 asyncpg PoC + A5 particionamiento

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
This commit is contained in:
2026-04-27 09:53:36 +00:00
parent 042acd6207
commit a1be8dd0ea
15 changed files with 998 additions and 145 deletions

View File

@@ -298,52 +298,66 @@ var Fleet = (function() {
});
}
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 rows = [];
var items = [];
results.forEach(function(r) {
var v = r.vehicle;
r.schedules.forEach(function(s) {
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';
rows.push(
'<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>'
);
items.push({vehicle: r.vehicle, schedule: s});
});
});
if (!rows.length) {
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;
}
body.innerHTML = rows.join('');
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 ───
@@ -371,6 +385,21 @@ var Fleet = (function() {
});
}
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 = [];
@@ -393,20 +422,16 @@ var Fleet = (function() {
return;
}
var html = '';
allLogs.forEach(function(l) {
html += '<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>';
});
body.innerHTML = html;
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 ───