feat(ui): mobile UI polish with segmented controls and responsive navbar
- Replace filter pills with btn-group segmented controls on mobile (all pages) - Add renderMobileSegmented() shared utility with colored count badges - Compact sync card and logs run selector on mobile - Unified flat-row format: dot + date + name + count (0.875rem throughout) - Responsive navbar with short labels on mobile (Acasa/Mapari/Lipsa/Jurnale) - Vertical dots icon (bi-three-dots-vertical) without dropdown caret - Shorter "Mapare" button text on mobile, Re-scan in context menu - Top pagination on logs page, hide per-page selector on mobile - Cache-bust static assets to v=5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,63 +5,65 @@
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">SKU-uri Lipsa</h4>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="exportMissingCsv()">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary d-none d-md-inline-flex" onclick="exportMissingCsv()">
|
||||
<i class="bi bi-download"></i> Export CSV
|
||||
</button>
|
||||
<!-- Mobile ⋯ dropdown -->
|
||||
<div class="dropdown d-md-none">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-three-dots-vertical"></i></button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="#" onclick="document.getElementById('rescanBtn').click();return false"><i class="bi bi-arrow-clockwise me-1"></i> Re-scan</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportMissingCsv();return false"><i class="bi bi-download me-1"></i> Export CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unified filter bar -->
|
||||
<div class="filter-bar" id="skusFilterBar">
|
||||
<button class="filter-pill active" data-sku-status="unresolved">
|
||||
Nerezolvate <span class="filter-count" id="cntUnres">0</span>
|
||||
<button class="filter-pill active d-none d-md-inline-flex" data-sku-status="unresolved">
|
||||
Nerezolvate <span class="filter-count fc-yellow" id="cntUnres">0</span>
|
||||
</button>
|
||||
<button class="filter-pill" data-sku-status="resolved">
|
||||
Rezolvate <span class="filter-count" id="cntRes">0</span>
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-sku-status="resolved">
|
||||
Rezolvate <span class="filter-count fc-green" id="cntRes">0</span>
|
||||
</button>
|
||||
<button class="filter-pill" data-sku-status="all">
|
||||
Toate <span class="filter-count" id="cntAllSkus">0</span>
|
||||
<button class="filter-pill d-none d-md-inline-flex" data-sku-status="all">
|
||||
Toate <span class="filter-count fc-neutral" id="cntAllSkus">0</span>
|
||||
</button>
|
||||
<input type="search" id="skuSearch" placeholder="Cauta SKU / produs..." class="search-input">
|
||||
<button id="rescanBtn" class="btn btn-sm btn-secondary ms-2">↻ Re-scan</button>
|
||||
<button id="rescanBtn" class="btn btn-sm btn-secondary ms-2 d-none d-md-inline-flex">↻ Re-scan</button>
|
||||
<span id="rescanProgress" class="align-items-center gap-2 text-primary" style="display:none;">
|
||||
<span class="sync-live-dot"></span>
|
||||
<span id="rescanProgressText">Scanare...</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-md-none mb-2" id="skusMobileSeg"></div>
|
||||
<!-- Result banner -->
|
||||
<div id="rescanResult" class="result-banner" style="display:none;margin-bottom:0.75rem;"></div>
|
||||
|
||||
<div id="skusPagTop" class="pag-strip mb-2"></div>
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div id="missingMobileList" class="mobile-list"></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>SKU</th>
|
||||
<th>Produs</th>
|
||||
<th>Nr. Comenzi</th>
|
||||
<th>Client</th>
|
||||
<th>First Seen</th>
|
||||
<th>Status</th>
|
||||
<th>Actiune</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="missingBody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Se incarca...</td></tr>
|
||||
<tr><td colspan="4" class="text-center text-muted py-4">Se incarca...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small class="text-muted" id="missingInfo"></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="paginationNav" class="mt-3">
|
||||
<ul class="pagination justify-content-center" id="paginationControls"></ul>
|
||||
</nav>
|
||||
<div id="skusPagBottom" class="pag-strip pag-strip-bottom"></div>
|
||||
|
||||
<!-- Map SKU Modal with multi-CODMAT support (R11) -->
|
||||
<div class="modal fade" id="mapModal" tabindex="-1">
|
||||
@@ -98,7 +100,9 @@ let currentMapSku = '';
|
||||
let mapAcTimeout = null;
|
||||
let currentPage = 1;
|
||||
let skuStatusFilter = 'unresolved';
|
||||
const perPage = 20;
|
||||
let missingPerPage = 20;
|
||||
|
||||
function missingChangePerPage(val) { missingPerPage = parseInt(val) || 20; currentPage = 1; loadMissingSkus(); }
|
||||
|
||||
// ── Filter pills ──────────────────────────────────
|
||||
document.querySelectorAll('.filter-pill[data-sku-status]').forEach(btn => {
|
||||
@@ -158,7 +162,7 @@ function loadMissingSkus(page) {
|
||||
const resolvedVal = resolvedParamFor(skuStatusFilter);
|
||||
params.set('resolved', resolvedVal);
|
||||
params.set('page', currentPage);
|
||||
params.set('per_page', perPage);
|
||||
params.set('per_page', missingPerPage);
|
||||
const search = document.getElementById('skuSearch')?.value?.trim();
|
||||
if (search) params.set('search', search);
|
||||
|
||||
@@ -170,12 +174,27 @@ function loadMissingSkus(page) {
|
||||
if (el('cntUnres')) el('cntUnres').textContent = c.unresolved || 0;
|
||||
if (el('cntRes')) el('cntRes').textContent = c.resolved || 0;
|
||||
if (el('cntAllSkus')) el('cntAllSkus').textContent = c.total || 0;
|
||||
|
||||
// Mobile segmented control
|
||||
renderMobileSegmented('skusMobileSeg', [
|
||||
{ label: 'Nerez.', count: c.unresolved || 0, value: 'unresolved', active: skuStatusFilter === 'unresolved', colorClass: 'fc-yellow' },
|
||||
{ label: 'Rez.', count: c.resolved || 0, value: 'resolved', active: skuStatusFilter === 'resolved', colorClass: 'fc-green' },
|
||||
{ label: 'Toate', count: c.total || 0, value: 'all', active: skuStatusFilter === 'all', colorClass: 'fc-neutral' }
|
||||
], (val) => {
|
||||
document.querySelectorAll('.filter-pill[data-sku-status]').forEach(b => b.classList.remove('active'));
|
||||
const pill = document.querySelector(`.filter-pill[data-sku-status="${val}"]`);
|
||||
if (pill) pill.classList.add('active');
|
||||
skuStatusFilter = val;
|
||||
currentPage = 1;
|
||||
loadMissingSkus();
|
||||
});
|
||||
|
||||
renderMissingSkusTable(data.skus || data.missing_skus || [], data);
|
||||
renderPagination(data);
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('missingBody').innerHTML =
|
||||
`<tr><td colspan="7" class="text-center text-danger">${err.message}</td></tr>`;
|
||||
`<tr><td colspan="4" class="text-center text-danger">${err.message}</td></tr>`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,38 +203,24 @@ function loadMissing(page) { loadMissingSkus(page); }
|
||||
|
||||
function renderMissingSkusTable(skus, data) {
|
||||
const tbody = document.getElementById('missingBody');
|
||||
if (data) {
|
||||
document.getElementById('missingInfo').textContent =
|
||||
`Total: ${data.total || 0} | Pagina: ${data.page || 1} din ${data.pages || 1}`;
|
||||
}
|
||||
const mobileList = document.getElementById('missingMobileList');
|
||||
|
||||
if (!skus || skus.length === 0) {
|
||||
const msg = skuStatusFilter === 'unresolved' ? 'Toate SKU-urile sunt mapate!' :
|
||||
skuStatusFilter === 'resolved' ? 'Niciun SKU rezolvat' : 'Niciun SKU gasit';
|
||||
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-muted py-4">${msg}</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">${msg}</td></tr>`;
|
||||
if (mobileList) mobileList.innerHTML = `<div class="flat-row text-muted py-3 justify-content-center">${msg}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = skus.map(s => {
|
||||
const statusBadge = s.resolved
|
||||
? '<span class="badge bg-success">Rezolvat</span>'
|
||||
: '<span class="badge bg-warning">Nerezolvat</span>';
|
||||
|
||||
let firstCustomer = '-';
|
||||
try {
|
||||
const customers = JSON.parse(s.customers || '[]');
|
||||
if (customers.length > 0) firstCustomer = customers[0];
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
const orderCount = s.order_count != null ? s.order_count : '-';
|
||||
|
||||
return `<tr class="${s.resolved ? 'table-light' : ''}">
|
||||
const trAttrs = !s.resolved
|
||||
? ` style="cursor:pointer" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}')"`
|
||||
: '';
|
||||
return `<tr${trAttrs}>
|
||||
<td>${s.resolved ? '<span class="dot dot-green"></span>' : '<span class="dot dot-yellow"></span>'}</td>
|
||||
<td><code>${esc(s.sku)}</code></td>
|
||||
<td>${esc(s.product_name || '-')}</td>
|
||||
<td>${esc(orderCount)}</td>
|
||||
<td><small>${esc(firstCustomer)}</small></td>
|
||||
<td><small>${s.first_seen ? new Date(s.first_seen).toLocaleDateString('ro-RO') : '-'}</small></td>
|
||||
<td>${statusBadge}</td>
|
||||
<td class="truncate" style="max-width:300px">${esc(s.product_name || '-')}</td>
|
||||
<td>
|
||||
${!s.resolved
|
||||
? `<a href="#" class="btn-map-icon" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;" title="Mapeaza">
|
||||
@@ -225,31 +230,33 @@ function renderMissingSkusTable(skus, data) {
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
if (mobileList) {
|
||||
mobileList.innerHTML = skus.map(s => {
|
||||
const actionHtml = !s.resolved
|
||||
? `<a href="#" class="btn-map-icon" onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;"><i class="bi bi-link-45deg"></i></a>`
|
||||
: `<small class="text-muted">${s.resolved_at ? new Date(s.resolved_at).toLocaleDateString('ro-RO') : ''}</small>`;
|
||||
const flatRowAttrs = !s.resolved
|
||||
? ` onclick="openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}')" style="cursor:pointer"`
|
||||
: '';
|
||||
return `<div class="flat-row"${flatRowAttrs}>
|
||||
${s.resolved ? '<span class="dot dot-green"></span>' : '<span class="dot dot-yellow"></span>'}
|
||||
<code class="me-1 text-nowrap">${esc(s.sku)}</code>
|
||||
<span class="grow truncate">${esc(s.product_name || '-')}</span>
|
||||
${actionHtml}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(data) {
|
||||
const ul = document.getElementById('paginationControls');
|
||||
const total = data.pages || 1;
|
||||
const page = data.page || 1;
|
||||
if (total <= 1) { ul.innerHTML = ''; return; }
|
||||
|
||||
let html = '';
|
||||
html += `<li class="page-item ${page <= 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadMissingSkus(${page - 1}); return false;">Anterior</a></li>`;
|
||||
|
||||
const range = 2;
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (i === 1 || i === total || (i >= page - range && i <= page + range)) {
|
||||
html += `<li class="page-item ${i === page ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadMissingSkus(${i}); return false;">${i}</a></li>`;
|
||||
} else if (i === page - range - 1 || i === page + range + 1) {
|
||||
html += `<li class="page-item disabled"><span class="page-link">…</span></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<li class="page-item ${page >= total ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadMissingSkus(${page + 1}); return false;">Urmator</a></li>`;
|
||||
ul.innerHTML = html;
|
||||
const pagOpts = { perPage: missingPerPage, perPageFn: 'missingChangePerPage', perPageOptions: [20, 50, 100] };
|
||||
const infoHtml = `<small class="text-muted me-auto">Total: ${data.total || 0} | Pagina ${data.page || 1} din ${data.pages || 1}</small>`;
|
||||
const pagHtml = infoHtml + renderUnifiedPagination(data.page || 1, data.pages || 1, 'loadMissing', pagOpts);
|
||||
const top = document.getElementById('skusPagTop');
|
||||
const bot = document.getElementById('skusPagBottom');
|
||||
if (top) top.innerHTML = pagHtml;
|
||||
if (bot) bot.innerHTML = pagHtml;
|
||||
}
|
||||
|
||||
// ── Multi-CODMAT Map Modal ───────────────────────
|
||||
@@ -384,9 +391,5 @@ function exportMissingCsv() {
|
||||
window.location.href = '/api/validate/missing-skus-csv';
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user