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:
Claude Agent
2026-03-15 21:20:24 +00:00
parent 5a0ea462e5
commit 680f670037
10 changed files with 931 additions and 549 deletions

View File

@@ -10,14 +10,6 @@ let currentQmOrderNumber = '';
let ordersSortColumn = 'order_date';
let ordersSortDirection = 'desc';
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function fmtDuration(startedAt, finishedAt) {
if (!startedAt || !finishedAt) return '-';
const diffMs = new Date(finishedAt) - new Date(startedAt);
@@ -27,24 +19,12 @@ function fmtDuration(startedAt, finishedAt) {
return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
}
function fmtDate(dateStr) {
if (!dateStr) return '-';
try {
const d = new Date(dateStr);
const hasTime = dateStr.includes(':');
if (hasTime) {
return d.toLocaleString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch { return dateStr; }
}
function runStatusBadge(status) {
switch ((status || '').toLowerCase()) {
case 'completed': return '<span class="badge bg-success">completed</span>';
case 'running': return '<span class="badge bg-primary">running</span>';
case 'failed': return '<span class="badge bg-danger">failed</span>';
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
case 'completed': return '<span style="color:#16a34a;font-weight:600">completed</span>';
case 'running': return '<span style="color:#2563eb;font-weight:600">running</span>';
case 'failed': return '<span style="color:#dc2626;font-weight:600">failed</span>';
default: return `<span style="font-weight:600">${esc(status)}</span>`;
}
}
@@ -58,6 +38,18 @@ function orderStatusBadge(status) {
}
}
function logStatusText(status) {
switch ((status || '').toUpperCase()) {
case 'IMPORTED': return 'Importat';
case 'ALREADY_IMPORTED': return 'Deja imp.';
case 'SKIPPED': return 'Omis';
case 'ERROR': return 'Eroare';
default: return esc(status);
}
}
function logsGoPage(p) { loadRunOrders(currentRunId, null, p); }
// ── Runs Dropdown ────────────────────────────────
async function loadRuns() {
@@ -88,6 +80,8 @@ async function loadRuns() {
return `<option value="${esc(r.run_id)}" ${selected}>${esc(label)}</option>`;
}).join('');
}
const ddMobile = document.getElementById('runsDropdownMobile');
if (ddMobile) ddMobile.innerHTML = dd.innerHTML;
} catch (err) {
const dd = document.getElementById('runsDropdown');
dd.innerHTML = `<option value="">Eroare: ${esc(err.message)}</option>`;
@@ -110,6 +104,8 @@ async function selectRun(runId) {
// Sync dropdown selection
const dd = document.getElementById('runsDropdown');
if (dd && dd.value !== runId) dd.value = runId;
const ddMobile = document.getElementById('runsDropdownMobile');
if (ddMobile && ddMobile.value !== runId) ddMobile.value = runId;
if (!runId) {
document.getElementById('logViewerSection').style.display = 'none';
@@ -117,8 +113,8 @@ async function selectRun(runId) {
}
document.getElementById('logViewerSection').style.display = '';
document.getElementById('logRunId').textContent = runId;
document.getElementById('logStatusBadge').innerHTML = '<span class="badge bg-secondary">...</span>';
const logRunIdEl = document.getElementById('logRunId'); if (logRunIdEl) logRunIdEl.textContent = runId;
document.getElementById('logStatusBadge').innerHTML = '...';
document.getElementById('textLogSection').style.display = 'none';
await loadRunOrders(runId, 'all', 1);
@@ -133,13 +129,9 @@ async function loadRunOrders(runId, statusFilter, page) {
if (statusFilter != null) currentFilter = statusFilter;
if (page != null) ordersPage = page;
// Update filter button styles
document.querySelectorAll('#orderFilterBtns button').forEach(btn => {
btn.className = btn.className.replace(' btn-primary', ' btn-outline-primary')
.replace(' btn-success', ' btn-outline-success')
.replace(' btn-info', ' btn-outline-info')
.replace(' btn-warning', ' btn-outline-warning')
.replace(' btn-danger', ' btn-outline-danger');
// Update filter pill active state
document.querySelectorAll('#orderFilterPills .filter-pill').forEach(btn => {
btn.classList.toggle('active', btn.dataset.logStatus === currentFilter);
});
try {
@@ -155,15 +147,6 @@ async function loadRunOrders(runId, statusFilter, page) {
const alreadyEl = document.getElementById('countAlreadyImported');
if (alreadyEl) alreadyEl.textContent = counts.already_imported || 0;
// Highlight active filter
const filterMap = { 'all': 0, 'IMPORTED': 1, 'ALREADY_IMPORTED': 2, 'SKIPPED': 3, 'ERROR': 4 };
const btns = document.querySelectorAll('#orderFilterBtns button');
const idx = filterMap[currentFilter] ?? 0;
if (btns[idx]) {
const colorMap = ['primary', 'success', 'info', 'warning', 'danger'];
btns[idx].className = btns[idx].className.replace(`btn-outline-${colorMap[idx]}`, `btn-${colorMap[idx]}`);
}
const tbody = document.getElementById('runOrdersBody');
const orders = data.orders || [];
@@ -178,32 +161,62 @@ async function loadRunOrders(runId, statusFilter, page) {
<td><code>${esc(o.order_number)}</code></td>
<td>${esc(o.customer_name)}</td>
<td>${o.items_count || 0}</td>
<td>${orderStatusBadge(o.status)}</td>
<td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
</tr>`;
}).join('');
}
// Mobile flat rows
const mobileList = document.getElementById('logsMobileList');
if (mobileList) {
if (orders.length === 0) {
mobileList.innerHTML = '<div class="flat-row text-muted py-3 justify-content-center">Nicio comanda</div>';
} else {
mobileList.innerHTML = orders.map(o => {
const d = o.order_date || '';
let dateFmt = '-';
if (d.length >= 10) {
dateFmt = d.slice(8, 10) + '.' + d.slice(5, 7) + '.' + d.slice(2, 4);
if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16);
}
const totalStr = o.order_total ? Math.round(o.order_total) : '';
return `<div class="flat-row" onclick="openOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)}
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
<span class="grow truncate">${esc(o.customer_name || '—')}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}</span>
</div>`;
}).join('');
}
}
// Mobile segmented control
renderMobileSegmented('logsMobileSeg', [
{ label: 'Toate', count: counts.total || 0, value: 'all', active: currentFilter === 'all', colorClass: 'fc-neutral' },
{ label: 'Imp.', count: counts.imported || 0, value: 'IMPORTED', active: currentFilter === 'IMPORTED', colorClass: 'fc-green' },
{ label: 'Deja', count: counts.already_imported || 0, value: 'ALREADY_IMPORTED', active: currentFilter === 'ALREADY_IMPORTED', colorClass: 'fc-blue' },
{ label: 'Omise', count: counts.skipped || 0, value: 'SKIPPED', active: currentFilter === 'SKIPPED', colorClass: 'fc-yellow' },
{ label: 'Erori', count: counts.error || 0, value: 'ERROR', active: currentFilter === 'ERROR', colorClass: 'fc-red' }
], (val) => filterOrders(val));
// Orders pagination
const totalPages = data.pages || 1;
const infoEl = document.getElementById('ordersPageInfo');
infoEl.textContent = `${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}`;
if (infoEl) infoEl.textContent = `${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}`;
const pagHtml = `<small class="text-muted me-auto">${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}</small>` + renderUnifiedPagination(ordersPage, totalPages, 'logsGoPage');
const pagDiv = document.getElementById('ordersPagination');
if (totalPages > 1) {
pagDiv.innerHTML = `
<button class="btn btn-sm btn-outline-secondary" ${ordersPage <= 1 ? 'disabled' : ''} onclick="loadRunOrders('${esc(runId)}', null, ${ordersPage - 1})"><i class="bi bi-chevron-left"></i></button>
<small class="text-muted">${ordersPage} / ${totalPages}</small>
<button class="btn btn-sm btn-outline-secondary" ${ordersPage >= totalPages ? 'disabled' : ''} onclick="loadRunOrders('${esc(runId)}', null, ${ordersPage + 1})"><i class="bi bi-chevron-right"></i></button>
`;
} else {
pagDiv.innerHTML = '';
}
if (pagDiv) pagDiv.innerHTML = pagHtml;
const pagDivTop = document.getElementById('ordersPaginationTop');
if (pagDivTop) pagDivTop.innerHTML = pagHtml;
// Update run status badge
const runRes = await fetch(`/api/sync/run/${encodeURIComponent(runId)}`);
const runData = await runRes.json();
if (runData.run) {
document.getElementById('logStatusBadge').innerHTML = runStatusBadge(runData.run.status);
// Update mobile run dot
const mDot = document.getElementById('mobileRunDot');
if (mDot) mDot.className = 'sync-status-dot ' + (runData.run.status || 'idle');
}
} catch (err) {
document.getElementById('runOrdersBody').innerHTML =
@@ -517,6 +530,12 @@ async function saveQuickMapping() {
document.addEventListener('DOMContentLoaded', () => {
loadRuns();
document.querySelectorAll('#orderFilterPills .filter-pill').forEach(btn => {
btn.addEventListener('click', function() {
filterOrders(this.dataset.logStatus || 'all');
});
});
const preselected = document.getElementById('preselectedRun');
const urlParams = new URLSearchParams(window.location.search);
const runFromUrl = urlParams.get('run') || (preselected ? preselected.value : '');
@@ -533,4 +552,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (logPollTimer) { clearInterval(logPollTimer); logPollTimer = null; }
}
});
document.getElementById('autoRefreshToggleMobile')?.addEventListener('change', (e) => {
const desktop = document.getElementById('autoRefreshToggle');
if (desktop) desktop.checked = e.target.checked;
desktop?.dispatchEvent(new Event('change'));
});
});