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

@@ -94,45 +94,53 @@ const Customers = (() => {
}
}
function renderCustomerRow(c) {
const tier = tierMap[c.price_tier] || 'Mostrador';
const tClass = tierClass[c.price_tier] || 'mostrador';
const limit = parseFloat(c.credit_limit || 0);
const balance = parseFloat(c.credit_balance || 0);
const available = Math.max(0, limit - balance);
const usedPct = limit > 0 ? Math.round((balance / limit) * 100) : 0;
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
const num = String(c.id).padStart(5, '0');
const selClass = (currentCustomer && currentCustomer.id === c.id) ? 'selected' : '';
return '<tr class="' + selClass + '" onclick="selectCustomer(' + c.id + ')">' +
'<td class="cell-num">' + num + '</td>' +
'<td>' +
'<div class="cell-name">' + (c.name || '') + '</div>' +
'<div class="cell-name-sub hide-mobile">' + (c.email || '') + '</div>' +
'</td>' +
'<td class="cell-rfc hide-mobile">' + (c.rfc || '-') + '</td>' +
'<td class="hide-mobile">' + (c.phone || '-') + '</td>' +
'<td class="hide-mobile" style="font-size:var(--text-caption);color:var(--color-text-secondary);">' + (c.email || '-') + '</td>' +
'<td><span class="tipo-chip tipo-chip--' + tClass + '">' + tier + '</span></td>' +
'<td class="cell-credit ' + creditClass + '">' + fmt(available) + '</td>' +
'<td class="cell-date hide-mobile">' + formatDate(c.last_purchase || c.created_at) + '</td>' +
'<td>' + statusBadge(c) + '</td>' +
'</tr>';
}
var customersVS = null;
function renderTable(customers) {
const tbody = document.getElementById('customersBody');
if (!tbody) return;
tbody.innerHTML = '';
if (!customers || customers.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>';
return;
}
customers.forEach((c, idx) => {
const tier = tierMap[c.price_tier] || 'Mostrador';
const tClass = tierClass[c.price_tier] || 'mostrador';
const limit = parseFloat(c.credit_limit || 0);
const balance = parseFloat(c.credit_balance || 0);
const available = Math.max(0, limit - balance);
const usedPct = limit > 0 ? Math.round((balance / limit) * 100) : 0;
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
const num = String(c.id).padStart(5, '0');
const tr = document.createElement('tr');
if (currentCustomer && currentCustomer.id === c.id) tr.className = 'selected';
tr.onclick = () => selectCustomer(c.id);
tr.innerHTML = `
<td class="cell-num">${num}</td>
<td>
<div class="cell-name">${c.name || ''}</div>
<div class="cell-name-sub hide-mobile">${c.email || ''}</div>
</td>
<td class="cell-rfc hide-mobile">${c.rfc || '-'}</td>
<td class="hide-mobile">${c.phone || '-'}</td>
<td class="hide-mobile" style="font-size:var(--text-caption);color:var(--color-text-secondary);">${c.email || '-'}</td>
<td><span class="tipo-chip tipo-chip--${tClass}">${tier}</span></td>
<td class="cell-credit ${creditClass}">${fmt(available)}</td>
<td class="cell-date hide-mobile">${formatDate(c.last_purchase || c.created_at)}</td>
<td>${statusBadge(c)}</td>
`;
tbody.appendChild(tr);
});
if (!customersVS) {
customersVS = new VirtualScroll({
container: tbody,
rowHeight: 52,
buffer: 3,
renderRow: renderCustomerRow,
emptyHtml: '<tr><td colspan="9" style="text-align:center;padding:var(--space-8);color:var(--color-text-muted);">Sin resultados.</td></tr>'
});
}
customersVS.setData(customers);
}
function renderPagination(pag) {