diff --git a/api/app/static/css/style.css b/api/app/static/css/style.css
index 8eae5c0..ed78f51 100644
--- a/api/app/static/css/style.css
+++ b/api/app/static/css/style.css
@@ -1,13 +1,5 @@
/* ── Design tokens ───────────────────────────────── */
:root {
- /* Sidebar */
- --sidebar-width: 224px;
- --sidebar-bg: #111827;
- --sidebar-text: #d1d5db;
- --sidebar-active-bg: #1f2937;
- --sidebar-active-text: #ffffff;
- --sidebar-border: #374151;
-
/* Surfaces */
--body-bg: #f9fafb;
--card-bg: #ffffff;
@@ -27,93 +19,89 @@
--text-secondary: #4b5563;
--text-muted: #6b7280;
--border-color: #e5e7eb;
+
+ /* Dots */
+ --dot-green: #22c55e;
+ --dot-yellow: #eab308;
+ --dot-red: #ef4444;
}
/* ── Base ────────────────────────────────────────── */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- font-size: 0.875rem;
+ font-size: 1rem;
background-color: var(--body-bg);
margin: 0;
padding: 0;
}
-/* ── Sidebar ─────────────────────────────────────── */
-.sidebar {
+/* ── Top Navbar ──────────────────────────────────── */
+.top-navbar {
position: fixed;
top: 0;
left: 0;
- width: var(--sidebar-width);
- height: 100vh;
- background-color: var(--sidebar-bg);
- padding: 0;
+ right: 0;
+ height: 48px;
+ background: #fff;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ padding: 0 1.5rem;
+ gap: 1.5rem;
z-index: 1000;
- overflow-y: auto;
- transition: transform 0.3s ease;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
-.sidebar-header {
- padding: 1.25rem 1rem;
- border-bottom: 1px solid var(--sidebar-border);
+.navbar-brand {
+ font-weight: 700;
+ font-size: 1rem;
+ color: #111827;
+ white-space: nowrap;
}
-.sidebar-header h5 {
- color: #fff;
- margin: 0;
- font-size: 1.1rem;
- font-weight: 600;
+.navbar-links {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
}
+.navbar-links::-webkit-scrollbar { display: none; }
-.sidebar .nav-link {
- color: var(--sidebar-text);
- font-size: 0.875rem;
+.nav-tab {
+ display: flex;
+ align-items: center;
+ padding: 0 1rem;
+ height: 48px;
+ color: #64748b;
+ text-decoration: none;
+ font-size: 0.9375rem;
font-weight: 500;
- padding: 0.5rem 0.75rem;
- border-radius: 0.375rem;
- margin: 0.125rem 0.5rem;
- transition: background 0.15s, color 0.15s;
+ border-bottom: 2px solid transparent;
+ white-space: nowrap;
+ flex-shrink: 0;
+ transition: color 0.15s, border-color 0.15s;
}
-
-.sidebar .nav-link:hover {
- color: var(--sidebar-active-text);
- background-color: var(--sidebar-active-bg);
+.nav-tab:hover {
+ color: #111827;
+ background: #f9fafb;
+ text-decoration: none;
}
-
-.sidebar .nav-link.active {
- color: var(--sidebar-active-text);
- background-color: var(--sidebar-active-bg);
-}
-
-.sidebar .nav-link i {
- margin-right: 0.5rem;
- width: 1.2rem;
- text-align: center;
-}
-
-.sidebar-footer {
- position: absolute;
- bottom: 0;
- padding: 0.75rem 1rem;
- border-top: 1px solid var(--sidebar-border);
- width: 100%;
+.nav-tab.active {
+ color: var(--blue-600);
+ border-bottom-color: var(--blue-600);
}
/* ── Main content ────────────────────────────────── */
.main-content {
- margin-left: var(--sidebar-width);
- padding: 1.5rem;
+ padding-top: 64px;
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ padding-bottom: 1.5rem;
min-height: 100vh;
}
-/* ── Sidebar toggle (mobile) ─────────────────────── */
-.sidebar-toggle {
- position: fixed;
- top: 0.5rem;
- left: 0.5rem;
- z-index: 1100;
- border-radius: 0.375rem;
-}
-
/* ── Cards ───────────────────────────────────────── */
.card {
border: none;
@@ -126,17 +114,17 @@ body {
background: var(--card-bg);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
padding: 0.75rem 1rem;
}
/* ── Tables ──────────────────────────────────────── */
.table {
- font-size: 0.875rem;
+ font-size: 1rem;
}
.table th {
- font-size: 0.75rem;
+ font-size: 0.8125rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
@@ -147,13 +135,14 @@ body {
}
.table td {
- padding: 0.75rem 1rem;
+ padding: 0.625rem 1rem;
color: var(--text-secondary);
+ font-size: 1rem;
}
/* ── Badges — soft pill style ────────────────────── */
.badge {
- font-size: 0.75rem;
+ font-size: 0.8125rem;
font-weight: 500;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
@@ -173,7 +162,7 @@ body {
/* ── Buttons ─────────────────────────────────────── */
.btn {
- font-size: 0.875rem;
+ font-size: 0.9375rem;
border-radius: 0.375rem;
}
@@ -193,7 +182,7 @@ body {
/* ── Forms ───────────────────────────────────────── */
.form-control, .form-select {
- font-size: 0.875rem;
+ font-size: 0.9375rem;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
border-color: #d1d5db;
@@ -204,12 +193,50 @@ body {
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
-/* ── Pagination ──────────────────────────────────── */
-.pagination .page-link {
- font-size: 0.875rem;
+/* ── Unified Pagination Bar ──────────────────────── */
+.pagination-bar {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ flex-wrap: wrap;
}
-/* ── Loading spinner ─────────────────────────────── */
+.page-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 2rem;
+ height: 2rem;
+ padding: 0 0.5rem;
+ font-size: 0.8125rem;
+ border: 1px solid #d1d5db;
+ border-radius: 0.375rem;
+ background: #fff;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: background 0.12s, border-color 0.12s;
+ text-decoration: none;
+ user-select: none;
+}
+.page-btn:hover:not(:disabled):not(.active) {
+ background: #f3f4f6;
+ border-color: #9ca3af;
+ color: var(--text-primary);
+ text-decoration: none;
+}
+.page-btn.active {
+ background: var(--blue-600);
+ border-color: var(--blue-600);
+ color: #fff;
+ font-weight: 600;
+}
+.page-btn:disabled, .page-btn.disabled {
+ opacity: 0.4;
+ cursor: default;
+ pointer-events: none;
+}
+
+/* Loading spinner ────────────────────────────────── */
.spinner-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
@@ -220,6 +247,42 @@ body {
justify-content: center;
}
+/* ── Colored dots ────────────────────────────────── */
+.dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+.dot-green { background: var(--dot-green); }
+.dot-yellow { background: var(--dot-yellow); }
+.dot-red { background: var(--dot-red); }
+.dot-gray { background: #9ca3af; }
+.dot-blue { background: #3b82f6; }
+
+/* ── Flat row (mobile + desktop) ────────────────── */
+.flat-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ border-bottom: 1px solid #f3f4f6;
+ font-size: 1rem;
+}
+.flat-row:last-child { border-bottom: none; }
+.flat-row:hover { background: #f9fafb; cursor: pointer; }
+
+.grow { flex: 1; min-width: 0; }
+.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
+/* ── Colored filter count - text color only ─────── */
+.fc-green { color: #16a34a; }
+.fc-yellow { color: #ca8a04; }
+.fc-red { color: #dc2626; }
+.fc-neutral { color: #6b7280; }
+.fc-blue { color: #2563eb; }
+
/* ── Log viewer (dark theme — keep as-is) ────────── */
.log-viewer {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
@@ -270,7 +333,7 @@ body {
/* ── Order detail modal ──────────────────────────── */
.modal-lg .table-sm td,
.modal-lg .table-sm th {
- font-size: 0.8125rem;
+ font-size: 0.875rem;
padding: 0.35rem 0.5rem;
}
@@ -320,7 +383,7 @@ tr.mapping-deleted td {
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: #fff;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
white-space: nowrap;
@@ -332,20 +395,12 @@ tr.mapping-deleted td {
color: #fff;
}
.filter-pill.active .filter-count {
- background: rgba(255,255,255,0.25);
- color: #fff;
+ color: rgba(255,255,255,0.9);
}
.filter-count {
- display: inline-block;
- min-width: 1.25rem;
- padding: 0 0.3rem;
- border-radius: 999px;
- background: #e5e7eb;
- font-size: 0.75rem;
+ font-size: 0.8125rem;
font-weight: 600;
- text-align: center;
- line-height: 1.4;
}
/* ── Search input ────────────────────────────────── */
@@ -354,7 +409,7 @@ tr.mapping-deleted td {
padding: 0.375rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
outline: none;
min-width: 180px;
}
@@ -375,7 +430,7 @@ tr.mapping-deleted td {
.autocomplete-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
border-bottom: 1px solid #f1f5f9;
}
.autocomplete-item:hover, .autocomplete-item.active {
@@ -387,7 +442,7 @@ tr.mapping-deleted td {
}
.autocomplete-item .denumire {
color: #64748b;
- font-size: 0.8rem;
+ font-size: 0.875rem;
}
/* ── Tooltip for Client/Cont ─────────────────────── */
@@ -439,7 +494,7 @@ tr.mapping-deleted td {
align-items: center;
gap: 1rem;
padding: 0.5rem 1rem;
- font-size: 0.875rem;
+ font-size: 1rem;
color: var(--text-muted);
cursor: pointer;
transition: background 0.12s;
@@ -451,7 +506,7 @@ tr.mapping-deleted td {
gap: 0.5rem;
padding: 0.4rem 1rem;
background: #eff6ff;
- font-size: 0.875rem;
+ font-size: 1rem;
color: var(--blue-700);
border-top: 1px solid #dbeafe;
}
@@ -489,14 +544,14 @@ tr.mapping-deleted td {
display: none;
gap: 0.375rem;
align-items: center;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
}
.period-custom-range.visible { display: flex; }
/* ── select-compact (used in filter bars) ─────────── */
.select-compact {
padding: 0.375rem 0.5rem;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: #fff;
@@ -506,14 +561,14 @@ tr.mapping-deleted td {
/* ── btn-compact (kept for backward compat) ──────── */
.btn-compact {
padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
}
/* ── Result banner ───────────────────────────────── */
.result-banner {
padding: 0.4rem 0.75rem;
border-radius: 0.375rem;
- font-size: 0.875rem;
+ font-size: 0.9375rem;
background: #d1fae5;
color: #065f46;
border: 1px solid #6ee7b7;
@@ -521,7 +576,7 @@ tr.mapping-deleted td {
/* ── Badge-pct (mappings page) ───────────────────── */
.badge-pct {
- font-size: 0.7rem;
+ font-size: 0.75rem;
padding: 0.1rem 0.35rem;
border-radius: 4px;
font-weight: 600;
@@ -529,10 +584,132 @@ tr.mapping-deleted td {
.badge-pct.complete { background: #d1fae5; color: #065f46; }
.badge-pct.incomplete { background: #fef3c7; color: #92400e; }
+/* ── Context Menu ────────────────────────────────── */
+.context-menu-trigger {
+ background: none;
+ border: none;
+ color: #9ca3af;
+ padding: 0.2rem 0.4rem;
+ cursor: pointer;
+ border-radius: 0.25rem;
+ font-size: 1rem;
+ line-height: 1;
+ transition: color 0.12s, background 0.12s;
+}
+.context-menu-trigger:hover {
+ color: var(--text-secondary);
+ background: #f3f4f6;
+}
+
+.context-menu {
+ position: fixed;
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
+ z-index: 1050;
+ min-width: 150px;
+ padding: 0.25rem 0;
+}
+.context-menu-item {
+ display: block;
+ width: 100%;
+ text-align: left;
+ padding: 0.45rem 0.9rem;
+ font-size: 0.9375rem;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-primary);
+ transition: background 0.1s;
+}
+.context-menu-item:hover { background: #f3f4f6; }
+.context-menu-item.text-danger { color: #dc2626; }
+.context-menu-item.text-danger:hover { background: #fee2e2; }
+
+/* ── Pagination info strip ───────────────────────── */
+.pag-strip {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid var(--border-color);
+ flex-wrap: wrap;
+}
+.pag-strip-bottom {
+ border-bottom: none;
+ border-top: 1px solid var(--border-color);
+}
+
+/* ── Per page selector ───────────────────────────── */
+.per-page-label {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+ font-size: 0.9375rem;
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+
+/* ── Mobile list vs desktop table ────────────────── */
+.mobile-list { display: none; }
+
+/* ── Mappings flat-rows: always visible ────────────── */
+.mappings-flat-list { display: block; }
+
+/* ── Mobile ⋯ dropdown ─────────────────────────── */
+.mobile-more-dropdown { position: relative; display: inline-block; }
+.mobile-more-dropdown .dropdown-toggle::after { display: none; }
+
+/* ── Mobile segmented control (hidden on desktop) ── */
+.mobile-seg { display: none; }
+
/* ── Responsive ──────────────────────────────────── */
@media (max-width: 767.98px) {
- .sidebar { transform: translateX(-100%); }
- .sidebar.show { transform: translateX(0); }
- .main-content { margin-left: 0; }
- .sidebar-toggle { display: block !important; }
+ .top-navbar {
+ padding: 0 0.5rem;
+ gap: 0.5rem;
+ }
+ .navbar-brand {
+ font-size: 0.875rem;
+ }
+ .nav-tab {
+ padding: 0 0.625rem;
+ font-size: 0.8125rem;
+ }
+ .main-content {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ }
+ .filter-bar {
+ gap: 0.375rem;
+ }
+ .filter-pill { padding: 0.25rem 0.5rem; font-size: 0.8125rem; }
+ .search-input { min-width: 0; width: 100%; order: 99; }
+ .page-btn.page-number { display: none; }
+ .page-btn.page-ellipsis { display: none; }
+ .table-responsive { display: none; }
+ .mobile-list { display: block; }
+
+ /* Segmented filter control (replaces pills on mobile) */
+ .filter-bar .filter-pill { display: none; }
+ .filter-bar .mobile-seg { display: flex; }
+
+ /* Sync card compact */
+ .sync-card-controls {
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 0.375rem;
+ padding: 0.5rem 0.75rem;
+ }
+ .sync-card-info {
+ flex-wrap: wrap;
+ gap: 0.375rem;
+ font-size: 0.8rem;
+ padding: 0.375rem 0.75rem;
+ }
+
+ /* Hide per-page selector on mobile */
+ .per-page-label { display: none; }
}
diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js
index 1214f72..5746140 100644
--- a/api/app/static/js/dashboard.js
+++ b/api/app/static/js/dashboard.js
@@ -92,14 +92,13 @@ function updateSyncPanel(data) {
const st = document.getElementById('lastSyncStatus');
if (d) d.textContent = lr.started_at ? lr.started_at.replace('T', ' ').slice(0, 16) : '\u2014';
if (dur) dur.textContent = lr.duration_seconds ? Math.round(lr.duration_seconds) + 's' : '\u2014';
- // Updated counts: ↑new =already ⊘skipped ✕errors
if (cnt) {
const newImp = lr.new_imported || 0;
const already = lr.already_imported || 0;
if (already > 0) {
- cnt.textContent = '\u2191' + newImp + ' =' + already + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
+ cnt.innerHTML = `${newImp} noi, ${already} deja ${lr.skipped || 0} omise ${lr.errors || 0} erori`;
} else {
- cnt.textContent = '\u2191' + (lr.imported || 0) + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
+ cnt.innerHTML = `${lr.imported || 0} imp. ${lr.skipped || 0} omise ${lr.errors || 0} erori`;
}
}
if (st) {
@@ -300,13 +299,13 @@ async function loadDashOrders() {
if (o.status !== 'IMPORTED' && o.status !== 'ALREADY_IMPORTED') {
invoiceBadge = '-';
} else if (o.invoice && o.invoice.facturat) {
- invoiceBadge = `Facturat`;
+ invoiceBadge = `Facturat`;
if (o.invoice.serie_act || o.invoice.numar_act) {
invoiceBadge += `
${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}`;
}
invoiceTotal = o.invoice.total_cu_tva ? Number(o.invoice.total_cu_tva).toFixed(2) : '-';
} else {
- invoiceBadge = 'Nefacturat';
+ invoiceBadge = `Nefacturat`;
}
return `
@@ -314,7 +313,7 @@ async function loadDashOrders() {
| ${dateStr} |
${renderClientCell(o)}
${o.items_count || 0} |
- ${statusBadge} |
+ ${statusDot(o.status)} ${statusLabelText(o.status)} |
${o.id_comanda || '-'} |
${invoiceBadge} |
${invoiceTotal} |
@@ -322,20 +321,53 @@ async function loadDashOrders() {
}).join('');
}
+ // Mobile flat rows
+ const mobileList = document.getElementById('dashMobileList');
+ if (mobileList) {
+ if (orders.length === 0) {
+ mobileList.innerHTML = 'Nicio comanda
';
+ } 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 name = o.shipping_name || o.customer_name || o.billing_name || '\u2014';
+ const totalStr = o.order_total ? Math.round(o.order_total) : '';
+ return `
+ ${statusDot(o.status)}
+ ${dateFmt}
+ ${esc(name)}
+ x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}
+
`;
+ }).join('');
+ }
+ }
+
+ // Mobile segmented control
+ renderMobileSegmented('dashMobileSeg', [
+ { label: 'Toate', count: c.total || 0, value: 'all', active: (activeStatus || 'all') === 'all', colorClass: 'fc-neutral' },
+ { label: 'Imp.', count: c.imported_all || c.imported || 0, value: 'IMPORTED', active: activeStatus === 'IMPORTED', colorClass: 'fc-green' },
+ { label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' },
+ { label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' },
+ { label: 'Nefact.', count: c.uninvoiced || c.nefacturate || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-neutral' }
+ ], (val) => {
+ document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
+ const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);
+ if (pill) pill.classList.add('active');
+ dashPage = 1;
+ loadDashOrders();
+ });
+
// Pagination
const pag = data.pagination || {};
const totalPages = pag.total_pages || data.pages || 1;
const totalOrders = (data.counts || {}).total || data.total || 0;
- const pageInfo = `${totalOrders} comenzi | Pagina ${dashPage} din ${totalPages}`;
- document.getElementById('dashPageInfo').textContent = pageInfo;
- const pagInfoTop = document.getElementById('dashPageInfoTop');
- if (pagInfoTop) pagInfoTop.textContent = pageInfo;
- const pagHtml = totalPages > 1 ? `
-
- ${dashPage} / ${totalPages}
-
- ` : '';
+ const pagOpts = { perPage: dashPerPage, perPageFn: 'dashChangePerPage', perPageOptions: [25, 50, 100, 250] };
+ const pagHtml = `${totalOrders} comenzi | Pagina ${dashPage} din ${totalPages}` + renderUnifiedPagination(dashPage, totalPages, 'dashGoPage', pagOpts);
const pagDiv = document.getElementById('dashPagination');
if (pagDiv) pagDiv.innerHTML = pagHtml;
const pagDivTop = document.getElementById('dashPaginationTop');
@@ -396,16 +428,15 @@ function escHtml(s) {
// Alias kept for backward compat with inline handlers in modal
function esc(s) { return escHtml(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' });
- }
- return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
- } catch { return dateStr; }
+
+function statusLabelText(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 orderStatusBadge(status) {
diff --git a/api/app/static/js/logs.js b/api/app/static/js/logs.js
index 72007db..6827e09 100644
--- a/api/app/static/js/logs.js
+++ b/api/app/static/js/logs.js
@@ -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, '&').replace(//g, '>').replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
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 'completed';
- case 'running': return 'running';
- case 'failed': return 'failed';
- default: return `${esc(status)}`;
+ case 'completed': return 'completed';
+ case 'running': return 'running';
+ case 'failed': return 'failed';
+ default: return `${esc(status)}`;
}
}
@@ -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 ``;
}).join('');
}
+ const ddMobile = document.getElementById('runsDropdownMobile');
+ if (ddMobile) ddMobile.innerHTML = dd.innerHTML;
} catch (err) {
const dd = document.getElementById('runsDropdown');
dd.innerHTML = ``;
@@ -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 = '...';
+ 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) {
${esc(o.order_number)} |
${esc(o.customer_name)} |
${o.items_count || 0} |
- ${orderStatusBadge(o.status)} |
+ ${statusDot(o.status)} ${logStatusText(o.status)} |
`;
}).join('');
}
+ // Mobile flat rows
+ const mobileList = document.getElementById('logsMobileList');
+ if (mobileList) {
+ if (orders.length === 0) {
+ mobileList.innerHTML = 'Nicio comanda
';
+ } 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 `
+ ${statusDot(o.status)}
+ ${dateFmt}
+ ${esc(o.customer_name || '—')}
+ x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}
+
`;
+ }).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 = `${data.total || 0} comenzi | Pagina ${ordersPage} din ${totalPages}` + renderUnifiedPagination(ordersPage, totalPages, 'logsGoPage');
const pagDiv = document.getElementById('ordersPagination');
- if (totalPages > 1) {
- pagDiv.innerHTML = `
-
- ${ordersPage} / ${totalPages}
-
- `;
- } 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'));
+ });
});
diff --git a/api/app/static/js/mappings.js b/api/app/static/js/mappings.js
index 94ab0df..e75af2d 100644
--- a/api/app/static/js/mappings.js
+++ b/api/app/static/js/mappings.js
@@ -1,4 +1,5 @@
let currentPage = 1;
+let mappingsPerPage = 50;
let currentSearch = '';
let searchTimeout = null;
let sortColumn = 'sku';
@@ -69,6 +70,20 @@ function updatePctCounts(counts) {
if (elAll) elAll.textContent = counts.total || 0;
if (elComplete) elComplete.textContent = counts.complete || 0;
if (elIncomplete) elIncomplete.textContent = counts.incomplete || 0;
+
+ // Mobile segmented control
+ renderMobileSegmented('mappingsMobileSeg', [
+ { label: 'Toate', count: counts.total || 0, value: 'all', active: pctFilter === 'all', colorClass: 'fc-neutral' },
+ { label: 'Complete', count: counts.complete || 0, value: 'complete', active: pctFilter === 'complete', colorClass: 'fc-green' },
+ { label: 'Incompl.', count: counts.incomplete || 0, value: 'incomplete', active: pctFilter === 'incomplete', colorClass: 'fc-yellow' }
+ ], (val) => {
+ document.querySelectorAll('.filter-pill[data-pct]').forEach(b => b.classList.remove('active'));
+ const pill = document.querySelector(`.filter-pill[data-pct="${val}"]`);
+ if (pill) pill.classList.add('active');
+ pctFilter = val;
+ currentPage = 1;
+ loadMappings();
+ });
}
// ── Load & Render ────────────────────────────────
@@ -79,7 +94,7 @@ async function loadMappings() {
const params = new URLSearchParams({
search: currentSearch,
page: currentPage,
- per_page: 50,
+ per_page: mappingsPerPage,
sort_by: sortColumn,
sort_dir: sortDirection
});
@@ -103,116 +118,129 @@ async function loadMappings() {
renderPagination(data);
updateSortIcons();
} catch (err) {
- document.getElementById('mappingsBody').innerHTML =
- `| Eroare: ${err.message} |
`;
+ document.getElementById('mappingsFlatList').innerHTML =
+ `Eroare: ${err.message}
`;
}
}
function renderTable(mappings, showDeleted) {
- const tbody = document.getElementById('mappingsBody');
+ const container = document.getElementById('mappingsFlatList');
if (!mappings || mappings.length === 0) {
- tbody.innerHTML = '| Nu exista mapari |
';
+ container.innerHTML = 'Nu exista mapari
';
return;
}
- // Group by SKU for visual grouping (R6)
- let html = '';
let prevSku = null;
- let groupIdx = 0;
- let skuGroupCounts = {};
-
- // Count items per SKU
+ let html = '';
mappings.forEach(m => {
- skuGroupCounts[m.sku] = (skuGroupCounts[m.sku] || 0) + 1;
- });
-
- mappings.forEach((m, i) => {
const isNewGroup = m.sku !== prevSku;
- if (isNewGroup) groupIdx++;
- const groupClass = groupIdx % 2 === 0 ? 'sku-group-even' : 'sku-group-odd';
- const isMulti = skuGroupCounts[m.sku] > 1;
- const inactiveClass = !m.activ && !m.sters ? 'table-secondary opacity-75' : '';
- const deletedClass = m.sters ? 'mapping-deleted' : '';
-
- // SKU cell: show only on first row of group
- let skuCell, productCell;
if (isNewGroup) {
- const badge = isMulti ? ` Set (${skuGroupCounts[m.sku]})` : '';
- // Percentage total badge
let pctBadge = '';
if (m.pct_total !== undefined) {
- if (m.is_complete) {
- pctBadge = ` ✓ 100%`;
- } else {
- const pctVal = typeof m.pct_total === 'number' ? m.pct_total.toFixed(2) : m.pct_total;
- pctBadge = ` ⚠ ${pctVal}%`;
- }
+ pctBadge = m.is_complete
+ ? ` ✓ 100%`
+ : ` ${typeof m.pct_total === 'number' ? m.pct_total.toFixed(0) : m.pct_total}%`;
}
- skuCell = `${esc(m.sku)}${badge}${pctBadge} | `;
- productCell = `${esc(m.product_name || '-')} | `;
- } else {
- skuCell = '';
- productCell = '';
+ const inactiveStyle = !m.activ && !m.sters ? 'opacity:0.6;' : '';
+ html += `
+
+ ${esc(m.sku)}${pctBadge}
+ ${esc(m.product_name || '')}
+ ${m.sters
+ ? ``
+ : ``
+ }
+
`;
}
-
- html += `
- ${skuCell}
- ${productCell}
- ${esc(m.codmat)} |
- ${esc(m.denumire || '-')} |
- ${esc(m.um || '-')} |
- ${m.cantitate_roa} |
- ${m.procent_pret}% |
-
-
- ${m.activ ? 'Activ' : 'Inactiv'}
-
- |
-
- ${m.sters ? `` : `
-
- `}
- |
-
`;
-
+ const deletedStyle = m.sters ? 'text-decoration:line-through;opacity:0.5;' : '';
+ html += `
+ ${esc(m.codmat)}
+ ${esc(m.denumire || '')}
+
+ x${m.cantitate_roa}
+ · ${m.procent_pret}%
+
+
`;
prevSku = m.sku;
});
+ container.innerHTML = html;
- tbody.innerHTML = html;
+ // Wire context menu triggers
+ container.querySelectorAll('.context-menu-trigger').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const { sku, codmat, cantitate, procent } = btn.dataset;
+ const rect = btn.getBoundingClientRect();
+ showContextMenu(rect.left, rect.bottom + 2, [
+ { label: 'Editeaza', action: () => openEditModal(sku, codmat, parseFloat(cantitate), parseFloat(procent)) },
+ { label: 'Sterge', action: () => deleteMappingConfirm(sku, codmat), danger: true }
+ ]);
+ });
+ });
+}
+
+// Inline edit for flat-row values (cantitate / procent)
+function editFlatValue(span, sku, codmat, field, currentValue) {
+ if (span.querySelector('input')) return;
+
+ const input = document.createElement('input');
+ input.type = 'number';
+ input.className = 'form-control form-control-sm d-inline';
+ input.value = currentValue;
+ input.step = field === 'cantitate_roa' ? '0.001' : '0.01';
+ input.style.width = '70px';
+ input.style.display = 'inline';
+
+ const originalText = span.textContent;
+ span.textContent = '';
+ span.appendChild(input);
+ input.focus();
+ input.select();
+
+ const save = async () => {
+ const newValue = parseFloat(input.value);
+ if (isNaN(newValue) || newValue === currentValue) {
+ span.textContent = originalText;
+ return;
+ }
+ try {
+ const body = {};
+ body[field] = newValue;
+ const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ });
+ const data = await res.json();
+ if (data.success) { loadMappings(); }
+ else { span.textContent = originalText; alert('Eroare: ' + (data.error || 'Update failed')); }
+ } catch (err) { span.textContent = originalText; }
+ };
+
+ input.addEventListener('blur', save);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') { e.preventDefault(); save(); }
+ if (e.key === 'Escape') { span.textContent = originalText; }
+ });
}
function renderPagination(data) {
- const info = document.getElementById('pageInfo');
- info.textContent = `${data.total} mapari | Pagina ${data.page} din ${data.pages || 1}`;
-
- const ul = document.getElementById('pagination');
- if (data.pages <= 1) { ul.innerHTML = ''; return; }
-
- let html = '';
- html += `
- «`;
-
- let start = Math.max(1, data.page - 3);
- let end = Math.min(data.pages, start + 6);
- start = Math.max(1, end - 6);
-
- for (let i = start; i <= end; i++) {
- html += `
- ${i}`;
- }
-
- html += `
- »`;
-
- ul.innerHTML = html;
+ const pagOpts = { perPage: mappingsPerPage, perPageFn: 'mappingsChangePerPage', perPageOptions: [25, 50, 100, 250] };
+ const infoHtml = `${data.total} mapari | Pagina ${data.page} din ${data.pages || 1}`;
+ const pagHtml = infoHtml + renderUnifiedPagination(data.page, data.pages || 1, 'goPage', pagOpts);
+ const top = document.getElementById('mappingsPagTop');
+ const bot = document.getElementById('mappingsPagBottom');
+ if (top) top.innerHTML = pagHtml;
+ if (bot) bot.innerHTML = pagHtml;
}
+function mappingsChangePerPage(val) { mappingsPerPage = parseInt(val) || 50; currentPage = 1; loadMappings(); }
+
function goPage(p) {
currentPage = p;
loadMappings();
@@ -411,36 +439,34 @@ async function saveMapping() {
let inlineAddVisible = false;
function showInlineAddRow() {
+ // On mobile, open the full modal instead
+ if (window.innerWidth < 768) {
+ new bootstrap.Modal(document.getElementById('addModal')).show();
+ return;
+ }
+
if (inlineAddVisible) return;
inlineAddVisible = true;
- const tbody = document.getElementById('mappingsBody');
- const row = document.createElement('tr');
+ const container = document.getElementById('mappingsFlatList');
+ const row = document.createElement('div');
row.id = 'inlineAddRow';
- row.className = 'table-info';
+ row.className = 'flat-row';
+ row.style.background = '#eff6ff';
+ row.style.gap = '0.5rem';
row.innerHTML = `
-
-
- |
-
+
+ |
- - |
-
-
- |
-
-
- |
- - |
-
-
-
- |
+
+
+
+
+
`;
- tbody.insertBefore(row, tbody.firstChild);
+ container.insertBefore(row, container.firstChild);
document.getElementById('inlineSku').focus();
// Setup autocomplete for inline CODMAT
@@ -515,51 +541,6 @@ function cancelInlineAdd() {
inlineAddVisible = false;
}
-// ── Inline Edit ──────────────────────────────────
-
-function editCell(td, sku, codmat, field, currentValue) {
- if (td.querySelector('input')) return;
-
- const input = document.createElement('input');
- input.type = 'number';
- input.className = 'form-control form-control-sm';
- input.value = currentValue;
- input.step = field === 'cantitate_roa' ? '0.001' : '0.01';
- input.style.width = '80px';
-
- const originalText = td.textContent;
- td.textContent = '';
- td.appendChild(input);
- input.focus();
- input.select();
-
- const save = async () => {
- const newValue = parseFloat(input.value);
- if (isNaN(newValue) || newValue === currentValue) {
- td.textContent = originalText;
- return;
- }
- try {
- const body = {};
- body[field] = newValue;
- const res = await fetch(`/api/mappings/${encodeURIComponent(sku)}/${encodeURIComponent(codmat)}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- });
- const data = await res.json();
- if (data.success) { loadMappings(); }
- else { td.textContent = originalText; alert('Eroare: ' + (data.error || 'Update failed')); }
- } catch (err) { td.textContent = originalText; }
- };
-
- input.addEventListener('blur', save);
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') save();
- if (e.key === 'Escape') { td.textContent = originalText; }
- });
-}
-
// ── Toggle Active with Toast Undo ────────────────
async function toggleActive(sku, codmat, currentActive) {
@@ -714,7 +695,3 @@ function handleMappingConflict(data) {
}
}
-function esc(s) {
- if (s == null) return '';
- return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
-}
diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js
new file mode 100644
index 0000000..b5206db
--- /dev/null
+++ b/api/app/static/js/shared.js
@@ -0,0 +1,214 @@
+// shared.js - Unified utilities for all pages
+
+// ── HTML escaping ─────────────────────────────────
+function esc(s) {
+ if (s == null) return '';
+ return String(s)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+// ── Date formatting ───────────────────────────────
+function fmtDate(dateStr, includeSeconds) {
+ if (!dateStr) return '-';
+ try {
+ const d = new Date(dateStr);
+ const hasTime = dateStr.includes(':');
+ if (hasTime) {
+ const opts = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' };
+ if (includeSeconds) opts.second = '2-digit';
+ return d.toLocaleString('ro-RO', opts);
+ }
+ return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
+ } catch { return dateStr; }
+}
+
+// ── Unified Pagination ────────────────────────────
+/**
+ * Renders a full pagination bar with First/Prev/numbers/Next/Last.
+ * @param {number} currentPage
+ * @param {number} totalPages
+ * @param {string} goToFnName - name of global function to call with page number
+ * @param {object} [opts] - optional: { perPage, perPageFn, perPageOptions }
+ * @returns {string} HTML string
+ */
+function renderUnifiedPagination(currentPage, totalPages, goToFnName, opts) {
+ if (totalPages <= 1 && !(opts && opts.perPage)) {
+ return '';
+ }
+
+ let html = '';
+
+ // Per-page selector
+ if (opts && opts.perPage && opts.perPageFn) {
+ const options = opts.perPageOptions || [25, 50, 100, 250];
+ html += `';
+ }
+
+ if (totalPages <= 1) {
+ html += '
';
+ return html;
+ }
+
+ html += '';
+ return html;
+}
+
+// ── Context Menu ──────────────────────────────────
+let _activeContextMenu = null;
+
+function closeAllContextMenus() {
+ if (_activeContextMenu) {
+ _activeContextMenu.remove();
+ _activeContextMenu = null;
+ }
+}
+
+document.addEventListener('click', closeAllContextMenus);
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') closeAllContextMenus();
+});
+
+/**
+ * Show a context menu at the given position.
+ * @param {number} x - clientX
+ * @param {number} y - clientY
+ * @param {Array} items - [{label, action, danger}]
+ */
+function showContextMenu(x, y, items) {
+ closeAllContextMenus();
+
+ const menu = document.createElement('div');
+ menu.className = 'context-menu';
+
+ items.forEach(item => {
+ const btn = document.createElement('button');
+ btn.className = 'context-menu-item' + (item.danger ? ' text-danger' : '');
+ btn.textContent = item.label;
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ closeAllContextMenus();
+ item.action();
+ });
+ menu.appendChild(btn);
+ });
+
+ document.body.appendChild(menu);
+ _activeContextMenu = menu;
+
+ // Position menu, keeping it within viewport
+ const rect = menu.getBoundingClientRect();
+ const vw = window.innerWidth;
+ const vh = window.innerHeight;
+ let left = x;
+ let top = y;
+ if (left + 160 > vw) left = vw - 165;
+ if (top + rect.height > vh) top = vh - rect.height - 5;
+ menu.style.left = left + 'px';
+ menu.style.top = top + 'px';
+}
+
+/**
+ * Wire right-click on desktop + three-dots button on mobile for a table.
+ * @param {string} rowSelector - CSS selector for clickable rows
+ * @param {function} menuItemsFn - called with row element, returns [{label, action, danger}]
+ */
+function initContextMenus(rowSelector, menuItemsFn) {
+ document.addEventListener('contextmenu', (e) => {
+ const row = e.target.closest(rowSelector);
+ if (!row) return;
+ e.preventDefault();
+ showContextMenu(e.clientX, e.clientY, menuItemsFn(row));
+ });
+
+ document.addEventListener('click', (e) => {
+ const trigger = e.target.closest('.context-menu-trigger');
+ if (!trigger) return;
+ const row = trigger.closest(rowSelector);
+ if (!row) return;
+ e.stopPropagation();
+ const rect = trigger.getBoundingClientRect();
+ showContextMenu(rect.left, rect.bottom + 2, menuItemsFn(row));
+ });
+}
+
+// ── Mobile segmented control ─────────────────────
+/**
+ * Render a Bootstrap btn-group segmented control for mobile.
+ * @param {string} containerId - ID of the container div
+ * @param {Array} pills - [{label, count, colorClass, value, active}]
+ * @param {function} onSelect - callback(value)
+ */
+function renderMobileSegmented(containerId, pills, onSelect) {
+ const container = document.getElementById(containerId);
+ if (!container) return;
+
+ const btnStyle = 'font-size:0.75rem;height:32px;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:0.25rem;flex:1;padding:0 0.25rem';
+
+ container.innerHTML = `${pills.map(p => {
+ const cls = p.active ? 'btn btn-primary' : 'btn btn-outline-secondary';
+ const countColor = (!p.active && p.colorClass) ? ` class="${p.colorClass}"` : '';
+ return ``;
+ }).join('')}
`;
+
+ container.querySelectorAll('[data-seg-value]').forEach(btn => {
+ btn.addEventListener('click', () => onSelect(btn.dataset.segValue));
+ });
+}
+
+// ── Dot helper ────────────────────────────────────
+function statusDot(status) {
+ switch ((status || '').toUpperCase()) {
+ case 'IMPORTED':
+ case 'ALREADY_IMPORTED':
+ case 'COMPLETED':
+ case 'RESOLVED':
+ return '';
+ case 'SKIPPED':
+ case 'UNRESOLVED':
+ case 'INCOMPLETE':
+ return '';
+ case 'ERROR':
+ case 'FAILED':
+ return '';
+ default:
+ return '';
+ }
+}
diff --git a/api/app/templates/base.html b/api/app/templates/base.html
index 4761fcf..fa04883 100644
--- a/api/app/templates/base.html
+++ b/api/app/templates/base.html
@@ -6,52 +6,27 @@
{% block title %}GoMag Import Manager{% endblock %}
-
+
-
-