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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user