Compare commits
12 Commits
9e5901a8fb
...
f68adbb072
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f68adbb072 | ||
|
|
eccd9dd753 | ||
|
|
73fe53394e | ||
|
|
039cbb1438 | ||
|
|
1353d4b8cf | ||
|
|
f1c7625ec7 | ||
|
|
a898666869 | ||
|
|
1cea8cace0 | ||
|
|
327f0e6ea2 | ||
|
|
c806ca2d81 | ||
|
|
952989d34b | ||
|
|
aa6e035c02 |
@@ -34,7 +34,7 @@ python api/test_integration.py # cu Oracle
|
||||
1. Download GoMag API → JSON → parse → validate SKU-uri → import Oracle
|
||||
2. Ordinea: **parteneri** (cauta/creeaza) → **adrese** → **comanda** → **factura cache**
|
||||
3. SKU lookup: ARTICOLE_TERTI (mapped) are prioritate fata de NOM_ARTICOLE (direct)
|
||||
4. Complex sets: un SKU → multiple CODMAT-uri cu `procent_pret` (trebuie sa fie sum=100%)
|
||||
4. Complex sets (kituri/pachete): un SKU → multiple CODMAT-uri cu `cantitate_roa`; preturile se preiau din lista de preturi Oracle
|
||||
5. Comenzi anulate (GoMag statusId=7): verifica daca au factura inainte de stergere din Oracle
|
||||
|
||||
### Statusuri comenzi
|
||||
@@ -51,6 +51,11 @@ python api/test_integration.py # cu Oracle
|
||||
- Dual policy: articolele sunt rutate la `id_pol_vanzare` sau `id_pol_productie` pe baza contului contabil (341/345 = productie)
|
||||
- Daca pretul lipseste, se insereaza automat pret=0
|
||||
|
||||
### Dashboard paginare
|
||||
- Contorul din paginare arata **totalul comenzilor** din perioada selectata (ex: "378 comenzi"), NU doar cele filtrate
|
||||
- Butoanele de filtru (Importat, Omise, Erori, Facturate, Nefacturate, Anulate) arata fiecare cate comenzi are pe langa total
|
||||
- Aceasta este comportamentul dorit: userul vede cate comenzi totale sunt, din care cate importate, cu erori etc.
|
||||
|
||||
### Invoice cache
|
||||
- Coloanele `factura_*` pe `orders` (SQLite), populate lazy din Oracle (`vanzari WHERE sters=0`)
|
||||
- Refresh complet: verifica facturi noi + facturi sterse + comenzi sterse din ROA
|
||||
|
||||
@@ -157,6 +157,11 @@ async def download_products(
|
||||
|
||||
products = data.get("products", [])
|
||||
if isinstance(products, dict):
|
||||
# GoMag returns products as {"1": {...}, "2": {...}} dict
|
||||
first_val = next(iter(products.values()), None) if products else None
|
||||
if isinstance(first_val, dict):
|
||||
products = list(products.values())
|
||||
else:
|
||||
products = [products]
|
||||
if isinstance(products, list):
|
||||
for p in products:
|
||||
|
||||
@@ -35,7 +35,7 @@ async def prepare_price_sync() -> dict:
|
||||
try:
|
||||
await db.execute(
|
||||
"INSERT INTO price_sync_runs (run_id, started_at, status) VALUES (?, ?, 'running')",
|
||||
(run_id, _now().isoformat())
|
||||
(run_id, _now().strftime("%d.%m.%Y %H:%M:%S"))
|
||||
)
|
||||
await db.commit()
|
||||
finally:
|
||||
@@ -212,7 +212,7 @@ async def _finish_run(run_id, status, log_lines, products_total=0,
|
||||
matched = ?, updated = ?, errors = ?,
|
||||
log_text = ?
|
||||
WHERE run_id = ?
|
||||
""", (_now().isoformat(), status, products_total, matched, updated, errors,
|
||||
""", (_now().strftime("%d.%m.%Y %H:%M:%S"), status, products_total, matched, updated, errors,
|
||||
"\n".join(log_lines), run_id))
|
||||
await db.commit()
|
||||
finally:
|
||||
|
||||
@@ -35,6 +35,18 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* ── Checkboxes — accessible size ────────────────── */
|
||||
input[type="checkbox"] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
accent-color: var(--blue-600);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Top Navbar ──────────────────────────────────── */
|
||||
.top-navbar {
|
||||
position: fixed;
|
||||
@@ -141,6 +153,7 @@ body {
|
||||
padding: 0.625rem 1rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Zebra striping */
|
||||
@@ -212,10 +225,10 @@ body {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
min-width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.875rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
background: #fff;
|
||||
@@ -356,11 +369,16 @@ body {
|
||||
.qm-row { display: flex; gap: 6px; align-items: center; }
|
||||
.qm-codmat-wrap { flex: 1; min-width: 0; }
|
||||
.qm-rm-btn { padding: 2px 6px; line-height: 1; }
|
||||
#qmCodmatLines .qm-selected:empty { display: none; }
|
||||
#quickMapModal .modal-body { padding-top: 12px; padding-bottom: 8px; }
|
||||
#quickMapModal .modal-header { padding: 10px 16px; }
|
||||
#quickMapModal .modal-header h5 { font-size: 0.95rem; margin: 0; }
|
||||
#quickMapModal .modal-footer { padding: 8px 16px; }
|
||||
#qmCodmatLines .qm-selected:empty,
|
||||
#codmatLines .qm-selected:empty { display: none; }
|
||||
#quickMapModal .modal-body,
|
||||
#addModal .modal-body { padding-top: 12px; padding-bottom: 8px; }
|
||||
#quickMapModal .modal-header,
|
||||
#addModal .modal-header { padding: 10px 16px; }
|
||||
#quickMapModal .modal-header h5,
|
||||
#addModal .modal-header h5 { font-size: 0.95rem; margin: 0; }
|
||||
#quickMapModal .modal-footer,
|
||||
#addModal .modal-footer { padding: 8px 16px; }
|
||||
|
||||
/* ── Deleted mapping rows ────────────────────────── */
|
||||
tr.mapping-deleted td {
|
||||
@@ -399,7 +417,7 @@ tr.mapping-deleted td {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
background: #fff;
|
||||
@@ -429,10 +447,12 @@ tr.mapping-deleted td {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9375rem;
|
||||
outline: none;
|
||||
width: 160px;
|
||||
}
|
||||
.search-input:focus { border-color: var(--blue-600); }
|
||||
.search-input:focus {
|
||||
border-color: var(--blue-600);
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
/* ── Autocomplete dropdown (keep as-is) ──────────── */
|
||||
.autocomplete-dropdown {
|
||||
|
||||
@@ -4,10 +4,6 @@ let dashPerPage = 50;
|
||||
let dashSortCol = 'order_date';
|
||||
let dashSortDir = 'desc';
|
||||
let dashSearchTimeout = null;
|
||||
let currentQmSku = '';
|
||||
let currentQmOrderNumber = '';
|
||||
let qmAcTimeout = null;
|
||||
|
||||
// Sync polling state
|
||||
let _pollInterval = null;
|
||||
let _lastSyncStatus = null;
|
||||
@@ -484,7 +480,7 @@ function renderCodmatCell(item) {
|
||||
return `<code>${esc(d.codmat)}</code>`;
|
||||
}
|
||||
return item.codmat_details.map(d =>
|
||||
`<div class="small"><code>${esc(d.codmat)}</code> <span class="text-muted">\xd7${d.cantitate_roa} (${d.procent_pret}%)</span></div>`
|
||||
`<div class="small"><code>${esc(d.codmat)}</code> <span class="text-muted">\xd7${d.cantitate_roa}</span></div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
@@ -591,7 +587,7 @@ async function openDashOrderDetail(orderNumber) {
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0));
|
||||
return `<div class="dif-item">
|
||||
<div class="dif-row">
|
||||
<span class="dif-sku dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}', ${idx})">${esc(item.sku)}</span>
|
||||
<span class="dif-sku dif-codmat-link" onclick="openDashQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}', ${idx})">${esc(item.sku)}</span>
|
||||
${codmatText}
|
||||
</div>
|
||||
<div class="dif-row">
|
||||
@@ -649,7 +645,7 @@ async function openDashOrderDetail(orderNumber) {
|
||||
let tableHtml = items.map((item, idx) => {
|
||||
const valoare = Number(item.price || 0) * Number(item.quantity || 0);
|
||||
return `<tr>
|
||||
<td><code class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare">${esc(item.sku)}</code></td>
|
||||
<td><code class="codmat-link" onclick="openDashQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}', ${idx})" title="Click pentru mapare">${esc(item.sku)}</code></td>
|
||||
<td>${esc(item.product_name || '-')}</td>
|
||||
<td>${renderCodmatCell(item)}</td>
|
||||
<td class="text-end">${item.quantity || 0}</td>
|
||||
@@ -757,163 +753,23 @@ function renderReceipt(items, order) {
|
||||
mobile.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Quick Map Modal ───────────────────────────────
|
||||
// ── Quick Map Modal (uses shared openQuickMap) ───
|
||||
|
||||
function openQuickMap(sku, productName, orderNumber, itemIdx) {
|
||||
currentQmSku = sku;
|
||||
currentQmOrderNumber = orderNumber;
|
||||
document.getElementById('qmSku').textContent = sku;
|
||||
document.getElementById('qmProductName').textContent = productName || '-';
|
||||
document.getElementById('qmPctWarning').style.display = 'none';
|
||||
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Check if this is a direct SKU (SKU=CODMAT in NOM_ARTICOLE)
|
||||
function openDashQuickMap(sku, productName, orderNumber, itemIdx) {
|
||||
const item = (window._detailItems || [])[itemIdx];
|
||||
const details = item?.codmat_details;
|
||||
const isDirect = details?.length === 1 && details[0].direct === true;
|
||||
const directInfo = document.getElementById('qmDirectInfo');
|
||||
const saveBtn = document.getElementById('qmSaveBtn');
|
||||
|
||||
if (isDirect) {
|
||||
if (directInfo) {
|
||||
directInfo.innerHTML = `<i class="bi bi-info-circle"></i> SKU = CODMAT direct in nomenclator (<code>${escHtml(details[0].codmat)}</code> — ${escHtml(details[0].denumire || '')}).<br><small class="text-muted">Poti suprascrie cu un alt CODMAT daca e necesar (ex: reambalare).</small>`;
|
||||
directInfo.style.display = '';
|
||||
}
|
||||
if (saveBtn) {
|
||||
saveBtn.textContent = 'Suprascrie mapare';
|
||||
}
|
||||
addQmCodmatLine();
|
||||
} else {
|
||||
if (directInfo) directInfo.style.display = 'none';
|
||||
if (saveBtn) saveBtn.textContent = 'Salveaza';
|
||||
|
||||
// Pre-populate with existing codmat_details if available
|
||||
if (details && details.length > 0) {
|
||||
details.forEach(d => {
|
||||
addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate_roa, procent: d.procent_pret, denumire: d.denumire });
|
||||
});
|
||||
} else {
|
||||
addQmCodmatLine();
|
||||
}
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('quickMapModal')).show();
|
||||
}
|
||||
|
||||
function addQmCodmatLine(prefill) {
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
const idx = container.children.length;
|
||||
const codmatVal = prefill?.codmat || '';
|
||||
const cantVal = prefill?.cantitate || 1;
|
||||
const pctVal = prefill?.procent || 100;
|
||||
const denumireVal = prefill?.denumire || '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'qm-line';
|
||||
div.innerHTML = `
|
||||
<div class="qm-row">
|
||||
<div class="qm-codmat-wrap position-relative">
|
||||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="CODMAT..." autocomplete="off" value="${escHtml(codmatVal)}">
|
||||
<div class="autocomplete-dropdown d-none qm-ac-dropdown"></div>
|
||||
</div>
|
||||
<input type="number" class="form-control form-control-sm qm-cantitate" value="${cantVal}" step="0.001" min="0.001" title="Cantitate ROA" style="width:70px">
|
||||
<input type="number" class="form-control form-control-sm qm-procent" value="${pctVal}" step="0.01" min="0" max="100" title="Procent %" style="width:70px">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger qm-rm-btn" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : '<span style="width:30px"></span>'}
|
||||
</div>
|
||||
<div class="qm-selected text-muted" style="font-size:0.75rem;padding-left:2px">${escHtml(denumireVal)}</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
const input = div.querySelector('.qm-codmat');
|
||||
const dropdown = div.querySelector('.qm-ac-dropdown');
|
||||
const selected = div.querySelector('.qm-selected');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(qmAcTimeout);
|
||||
qmAcTimeout = setTimeout(() => qmAutocomplete(input, dropdown, selected), 250);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
async function qmAutocomplete(input, dropdown, selectedEl) {
|
||||
const q = input.value;
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="qmSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function qmSelectArticle(el, codmat, label) {
|
||||
const line = el.closest('.qm-line');
|
||||
line.querySelector('.qm-codmat').value = codmat;
|
||||
line.querySelector('.qm-selected').textContent = label;
|
||||
line.querySelector('.qm-ac-dropdown').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function saveQuickMapping() {
|
||||
const lines = document.querySelectorAll('.qm-line');
|
||||
const mappings = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const codmat = line.querySelector('.qm-codmat').value.trim();
|
||||
const cantitate = parseFloat(line.querySelector('.qm-cantitate').value) || 1;
|
||||
const procent = parseFloat(line.querySelector('.qm-procent').value) || 100;
|
||||
if (!codmat) continue;
|
||||
mappings.push({ codmat, cantitate_roa: cantitate, procent_pret: procent });
|
||||
}
|
||||
|
||||
if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; }
|
||||
|
||||
if (mappings.length > 1) {
|
||||
const totalPct = mappings.reduce((s, m) => s + m.procent_pret, 0);
|
||||
if (Math.abs(totalPct - 100) > 0.01) {
|
||||
document.getElementById('qmPctWarning').textContent = `Suma procentelor trebuie sa fie 100% (actual: ${totalPct.toFixed(2)}%)`;
|
||||
document.getElementById('qmPctWarning').style.display = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
document.getElementById('qmPctWarning').style.display = 'none';
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (mappings.length === 1) {
|
||||
res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentQmSku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa, procent_pret: mappings[0].procent_pret })
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentQmSku, mappings })
|
||||
});
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('quickMapModal')).hide();
|
||||
if (currentQmOrderNumber) openDashOrderDetail(currentQmOrderNumber);
|
||||
openQuickMap({
|
||||
sku,
|
||||
productName,
|
||||
isDirect,
|
||||
directInfo: isDirect ? { codmat: details[0].codmat, denumire: details[0].denumire } : null,
|
||||
prefill: (!isDirect && details?.length) ? details.map(d => ({ codmat: d.codmat, cantitate: d.cantitate_roa, denumire: d.denumire })) : null,
|
||||
onSave: () => {
|
||||
if (orderNumber) openDashOrderDetail(orderNumber);
|
||||
loadDashOrders();
|
||||
} else {
|
||||
const msg = data.detail || data.error || 'Unknown';
|
||||
document.getElementById('qmPctWarning').textContent = msg;
|
||||
document.getElementById('qmPctWarning').style.display = '';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare: ' + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ let runsPage = 1;
|
||||
let logPollTimer = null;
|
||||
let currentFilter = 'all';
|
||||
let ordersPage = 1;
|
||||
let currentQmSku = '';
|
||||
let currentQmOrderNumber = '';
|
||||
let ordersSortColumn = 'order_date';
|
||||
let ordersSortDirection = 'desc';
|
||||
|
||||
@@ -310,7 +308,7 @@ function renderCodmatCell(item) {
|
||||
}
|
||||
// Multi-CODMAT: compact list
|
||||
return item.codmat_details.map(d =>
|
||||
`<div class="small"><code>${esc(d.codmat)}</code> <span class="text-muted">\xd7${d.cantitate_roa} (${d.procent_pret}%)</span></div>`
|
||||
`<div class="small"><code>${esc(d.codmat)}</code> <span class="text-muted">\xd7${d.cantitate_roa}</span></div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
@@ -384,8 +382,8 @@ async function openOrderDetail(orderNumber) {
|
||||
if (mobileContainer) {
|
||||
mobileContainer.innerHTML = '<div class="detail-item-flat">' + items.map((item, idx) => {
|
||||
const codmatList = item.codmat_details?.length
|
||||
? item.codmat_details.map(d => `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(d.codmat)}</span>`).join(' ')
|
||||
: `<span class="dif-codmat-link" onclick="openQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(item.codmat || '–')}</span>`;
|
||||
? item.codmat_details.map(d => `<span class="dif-codmat-link" onclick="openLogsQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(d.codmat)}</span>`).join(' ')
|
||||
: `<span class="dif-codmat-link" onclick="openLogsQuickMap('${esc(item.sku)}','${esc(item.product_name||'')}','${esc(orderNumber)}')">${esc(item.codmat || '–')}</span>`;
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||
return `<div class="dif-item">
|
||||
<div class="dif-row">
|
||||
@@ -403,7 +401,7 @@ async function openOrderDetail(orderNumber) {
|
||||
|
||||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||||
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
|
||||
const codmatCell = `<span class="codmat-link" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}')" title="Click pentru mapare">${renderCodmatCell(item)}</span>`;
|
||||
const codmatCell = `<span class="codmat-link" onclick="openLogsQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}')" title="Click pentru mapare">${renderCodmatCell(item)}</span>`;
|
||||
return `<tr>
|
||||
<td><code>${esc(item.sku)}</code></td>
|
||||
<td>${esc(item.product_name || '-')}</td>
|
||||
@@ -419,146 +417,17 @@ async function openOrderDetail(orderNumber) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quick Map Modal (from order detail) ──────────
|
||||
// ── Quick Map Modal (uses shared openQuickMap) ───
|
||||
|
||||
let qmAcTimeout = null;
|
||||
|
||||
function openQuickMap(sku, productName, orderNumber) {
|
||||
currentQmSku = sku;
|
||||
currentQmOrderNumber = orderNumber;
|
||||
document.getElementById('qmSku').textContent = sku;
|
||||
document.getElementById('qmProductName').textContent = productName || '-';
|
||||
document.getElementById('qmPctWarning').style.display = 'none';
|
||||
|
||||
// Reset CODMAT lines
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
container.innerHTML = '';
|
||||
addQmCodmatLine();
|
||||
|
||||
// Show quick map on top of order detail (modal stacking)
|
||||
new bootstrap.Modal(document.getElementById('quickMapModal')).show();
|
||||
}
|
||||
|
||||
function addQmCodmatLine() {
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
const idx = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded p-2 mb-2 qm-line';
|
||||
div.innerHTML = `
|
||||
<div class="mb-2 position-relative">
|
||||
<label class="form-label form-label-sm mb-1">CODMAT (Articol ROA)</label>
|
||||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="Cauta codmat sau denumire..." autocomplete="off">
|
||||
<div class="autocomplete-dropdown d-none qm-ac-dropdown"></div>
|
||||
<small class="text-muted qm-selected"></small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Cantitate ROA</label>
|
||||
<input type="number" class="form-control form-control-sm qm-cantitate" value="1" step="0.001" min="0.001">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label class="form-label form-label-sm mb-1">Procent Pret (%)</label>
|
||||
<input type="number" class="form-control form-control-sm qm-procent" value="100" step="0.01" min="0" max="100">
|
||||
</div>
|
||||
<div class="col-2 d-flex align-items-end">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
// Setup autocomplete on the new input
|
||||
const input = div.querySelector('.qm-codmat');
|
||||
const dropdown = div.querySelector('.qm-ac-dropdown');
|
||||
const selected = div.querySelector('.qm-selected');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(qmAcTimeout);
|
||||
qmAcTimeout = setTimeout(() => qmAutocomplete(input, dropdown, selected), 250);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
async function qmAutocomplete(input, dropdown, selectedEl) {
|
||||
const q = input.value;
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="qmSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function qmSelectArticle(el, codmat, label) {
|
||||
const line = el.closest('.qm-line');
|
||||
line.querySelector('.qm-codmat').value = codmat;
|
||||
line.querySelector('.qm-selected').textContent = label;
|
||||
line.querySelector('.qm-ac-dropdown').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function saveQuickMapping() {
|
||||
const lines = document.querySelectorAll('.qm-line');
|
||||
const mappings = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const codmat = line.querySelector('.qm-codmat').value.trim();
|
||||
const cantitate = parseFloat(line.querySelector('.qm-cantitate').value) || 1;
|
||||
const procent = parseFloat(line.querySelector('.qm-procent').value) || 100;
|
||||
if (!codmat) continue;
|
||||
mappings.push({ codmat, cantitate_roa: cantitate, procent_pret: procent });
|
||||
}
|
||||
|
||||
if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; }
|
||||
|
||||
// Validate percentage sum for multi-line
|
||||
if (mappings.length > 1) {
|
||||
const totalPct = mappings.reduce((s, m) => s + m.procent_pret, 0);
|
||||
if (Math.abs(totalPct - 100) > 0.01) {
|
||||
document.getElementById('qmPctWarning').textContent = `Suma procentelor trebuie sa fie 100% (actual: ${totalPct.toFixed(2)}%)`;
|
||||
document.getElementById('qmPctWarning').style.display = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
document.getElementById('qmPctWarning').style.display = 'none';
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (mappings.length === 1) {
|
||||
res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentQmSku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa, procent_pret: mappings[0].procent_pret })
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentQmSku, mappings })
|
||||
});
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('quickMapModal')).hide();
|
||||
// Refresh order detail items in the still-open modal
|
||||
if (currentQmOrderNumber) openOrderDetail(currentQmOrderNumber);
|
||||
// Refresh orders view
|
||||
function openLogsQuickMap(sku, productName, orderNumber) {
|
||||
openQuickMap({
|
||||
sku,
|
||||
productName,
|
||||
onSave: () => {
|
||||
if (orderNumber) openOrderDetail(orderNumber);
|
||||
loadRunOrders(currentRunId, currentFilter, ordersPage);
|
||||
} else {
|
||||
alert('Eroare: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare: ' + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Init ────────────────────────────────────────
|
||||
|
||||
@@ -368,21 +368,17 @@ function addCodmatLine() {
|
||||
const container = document.getElementById('codmatLines');
|
||||
const idx = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded p-2 mb-2 codmat-line';
|
||||
div.className = 'qm-line codmat-line';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col position-relative">
|
||||
<input type="text" class="form-control form-control-sm cl-codmat" placeholder="Cauta CODMAT..." autocomplete="off" data-idx="${idx}">
|
||||
<div class="qm-row">
|
||||
<div class="qm-codmat-wrap position-relative">
|
||||
<input type="text" class="form-control form-control-sm cl-codmat" placeholder="CODMAT..." autocomplete="off" data-idx="${idx}">
|
||||
<div class="autocomplete-dropdown d-none cl-ac-dropdown"></div>
|
||||
<small class="text-muted cl-selected"></small>
|
||||
</div>
|
||||
<div class="col-auto" style="width:90px">
|
||||
<input type="number" class="form-control form-control-sm cl-cantitate" value="1" step="0.001" min="0.001" placeholder="Cant." title="Cantitate ROA">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.codmat-line').remove()"><i class="bi bi-x-lg"></i></button>` : '<div style="width:31px"></div>'}
|
||||
</div>
|
||||
<input type="number" class="form-control form-control-sm cl-cantitate" value="1" step="0.001" min="0.001" title="Cantitate ROA" style="width:70px">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger qm-rm-btn" onclick="this.closest('.codmat-line').remove()"><i class="bi bi-x"></i></button>` : '<span style="width:30px"></span>'}
|
||||
</div>
|
||||
<div class="qm-selected text-muted cl-selected" style="font-size:0.75rem;padding-left:2px"></div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
|
||||
@@ -204,6 +204,154 @@ function renderMobileSegmented(containerId, pills, onSelect) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Shared Quick Map Modal ────────────────────────
|
||||
let _qmOnSave = null;
|
||||
let _qmAcTimeout = null;
|
||||
|
||||
/**
|
||||
* Open the shared quick-map modal.
|
||||
* @param {object} opts
|
||||
* @param {string} opts.sku
|
||||
* @param {string} opts.productName
|
||||
* @param {Array} [opts.prefill] - [{codmat, cantitate, denumire}]
|
||||
* @param {boolean}[opts.isDirect] - true if SKU=CODMAT direct
|
||||
* @param {object} [opts.directInfo] - {codmat, denumire} for direct SKU info
|
||||
* @param {function} opts.onSave - callback(sku, mappings) after successful save
|
||||
*/
|
||||
function openQuickMap(opts) {
|
||||
_qmOnSave = opts.onSave || null;
|
||||
document.getElementById('qmSku').textContent = opts.sku;
|
||||
document.getElementById('qmProductName').textContent = opts.productName || '-';
|
||||
document.getElementById('qmPctWarning').style.display = 'none';
|
||||
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
container.innerHTML = '';
|
||||
|
||||
const directInfo = document.getElementById('qmDirectInfo');
|
||||
const saveBtn = document.getElementById('qmSaveBtn');
|
||||
|
||||
if (opts.isDirect && opts.directInfo) {
|
||||
if (directInfo) {
|
||||
directInfo.innerHTML = `<i class="bi bi-info-circle"></i> SKU = CODMAT direct in nomenclator (<code>${esc(opts.directInfo.codmat)}</code> — ${esc(opts.directInfo.denumire || '')}).<br><small class="text-muted">Poti suprascrie cu un alt CODMAT daca e necesar (ex: reambalare).</small>`;
|
||||
directInfo.style.display = '';
|
||||
}
|
||||
if (saveBtn) saveBtn.textContent = 'Suprascrie mapare';
|
||||
addQmCodmatLine();
|
||||
} else {
|
||||
if (directInfo) directInfo.style.display = 'none';
|
||||
if (saveBtn) saveBtn.textContent = 'Salveaza';
|
||||
|
||||
if (opts.prefill && opts.prefill.length > 0) {
|
||||
opts.prefill.forEach(d => addQmCodmatLine({ codmat: d.codmat, cantitate: d.cantitate, denumire: d.denumire }));
|
||||
} else {
|
||||
addQmCodmatLine();
|
||||
}
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('quickMapModal')).show();
|
||||
}
|
||||
|
||||
function addQmCodmatLine(prefill) {
|
||||
const container = document.getElementById('qmCodmatLines');
|
||||
const idx = container.children.length;
|
||||
const codmatVal = prefill?.codmat || '';
|
||||
const cantVal = prefill?.cantitate || 1;
|
||||
const denumireVal = prefill?.denumire || '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'qm-line';
|
||||
div.innerHTML = `
|
||||
<div class="qm-row">
|
||||
<div class="qm-codmat-wrap position-relative">
|
||||
<input type="text" class="form-control form-control-sm qm-codmat" placeholder="CODMAT..." autocomplete="off" value="${esc(codmatVal)}">
|
||||
<div class="autocomplete-dropdown d-none qm-ac-dropdown"></div>
|
||||
</div>
|
||||
<input type="number" class="form-control form-control-sm qm-cantitate" value="${cantVal}" step="0.001" min="0.001" title="Cantitate ROA" style="width:70px">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger qm-rm-btn" onclick="this.closest('.qm-line').remove()"><i class="bi bi-x"></i></button>` : '<span style="width:30px"></span>'}
|
||||
</div>
|
||||
<div class="qm-selected text-muted" style="font-size:0.75rem;padding-left:2px">${esc(denumireVal)}</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
const input = div.querySelector('.qm-codmat');
|
||||
const dropdown = div.querySelector('.qm-ac-dropdown');
|
||||
const selected = div.querySelector('.qm-selected');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(_qmAcTimeout);
|
||||
_qmAcTimeout = setTimeout(() => _qmAutocomplete(input, dropdown, selected), 250);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
async function _qmAutocomplete(input, dropdown, selectedEl) {
|
||||
const q = input.value;
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="_qmSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function _qmSelectArticle(el, codmat, label) {
|
||||
const line = el.closest('.qm-line');
|
||||
line.querySelector('.qm-codmat').value = codmat;
|
||||
line.querySelector('.qm-selected').textContent = label;
|
||||
line.querySelector('.qm-ac-dropdown').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function saveQuickMapping() {
|
||||
const lines = document.querySelectorAll('#qmCodmatLines .qm-line');
|
||||
const mappings = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const codmat = line.querySelector('.qm-codmat').value.trim();
|
||||
const cantitate = parseFloat(line.querySelector('.qm-cantitate').value) || 1;
|
||||
if (!codmat) continue;
|
||||
mappings.push({ codmat, cantitate_roa: cantitate });
|
||||
}
|
||||
|
||||
if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; }
|
||||
|
||||
const sku = document.getElementById('qmSku').textContent;
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (mappings.length === 1) {
|
||||
res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa })
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku, mappings })
|
||||
});
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('quickMapModal')).hide();
|
||||
if (_qmOnSave) _qmOnSave(sku, mappings);
|
||||
} else {
|
||||
alert('Eroare: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dot helper ────────────────────────────────────
|
||||
function statusDot(status) {
|
||||
switch ((status || '').toUpperCase()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<html lang="ro" style="color-scheme: light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,7 +7,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||
{% set rp = request.scope.get('root_path', '') %}
|
||||
<link href="{{ rp }}/static/css/style.css?v=14" rel="stylesheet">
|
||||
<link href="{{ rp }}/static/css/style.css?v=17" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Top Navbar -->
|
||||
@@ -27,9 +27,41 @@
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Shared Quick Map Modal -->
|
||||
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom:8px; font-size:0.85rem">
|
||||
<small class="text-muted">Produs:</small> <strong id="qmProductName"></strong>
|
||||
</div>
|
||||
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
|
||||
<span style="flex:1">CODMAT</span>
|
||||
<span style="width:70px">Cant.</span>
|
||||
<span style="width:30px"></span>
|
||||
</div>
|
||||
<div id="qmCodmatLines"></div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
|
||||
+ CODMAT
|
||||
</button>
|
||||
<div id="qmDirectInfo" class="alert alert-info mt-2" style="display:none; font-size:0.85rem; padding:8px 12px;"></div>
|
||||
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
|
||||
<button type="button" class="btn btn-primary" id="qmSaveBtn" onclick="saveQuickMapping()">Salveaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>window.ROOT_PATH = "{{ rp }}";</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ rp }}/static/js/shared.js?v=11"></script>
|
||||
<script src="{{ rp }}/static/js/shared.js?v=12"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -165,41 +165,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Map Modal (used from order detail) -->
|
||||
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom:8px; font-size:0.85rem">
|
||||
<small class="text-muted">Produs:</small> <strong id="qmProductName"></strong>
|
||||
</div>
|
||||
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
|
||||
<span style="flex:1">CODMAT</span>
|
||||
<span style="width:70px">Cant.</span>
|
||||
<span style="width:70px">%</span>
|
||||
<span style="width:30px"></span>
|
||||
</div>
|
||||
<div id="qmCodmatLines">
|
||||
<!-- Dynamic CODMAT lines -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addQmCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
|
||||
+ CODMAT
|
||||
</button>
|
||||
<div id="qmDirectInfo" class="alert alert-info mt-2" style="display:none; font-size:0.85rem; padding:8px 12px;"></div>
|
||||
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
|
||||
<button type="button" class="btn btn-primary" id="qmSaveBtn" onclick="saveQuickMapping()">Salveaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=21"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/dashboard.js?v=24"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -151,37 +151,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Map Modal (used from order detail) -->
|
||||
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Mapeaza SKU: <code id="qmSku"></code></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Produs web:</small> <strong id="qmProductName"></strong>
|
||||
</div>
|
||||
<div id="qmCodmatLines">
|
||||
<!-- Dynamic CODMAT lines -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addQmCodmatLine()">
|
||||
<i class="bi bi-plus"></i> Adauga CODMAT
|
||||
</button>
|
||||
<div id="qmPctWarning" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveQuickMapping()">Salveaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden field for pre-selected run from URL/server -->
|
||||
<input type="hidden" id="preselectedRun" value="{{ selected_run }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/logs.js?v=9"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/logs.js?v=11"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -61,27 +61,31 @@
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal with multi-CODMAT support (R11) -->
|
||||
<div class="modal fade" id="addModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal fade" id="addModal" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addModalTitle">Adauga Mapare</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" class="form-control" id="inputSku" placeholder="Ex: 8714858124284">
|
||||
<div class="mb-2">
|
||||
<label class="form-label form-label-sm mb-1">SKU</label>
|
||||
<input type="text" class="form-control form-control-sm" id="inputSku" placeholder="Ex: 8714858124284">
|
||||
</div>
|
||||
<div class="mb-2" id="addModalProductName" style="display:none;">
|
||||
<small class="text-muted">Produs web:</small> <strong id="inputProductName"></strong>
|
||||
<div id="addModalProductName" style="display:none; margin-bottom:8px; font-size:0.85rem">
|
||||
<small class="text-muted">Produs:</small> <strong id="inputProductName"></strong>
|
||||
</div>
|
||||
<div class="qm-row" style="font-size:0.7rem; color:#9ca3af; padding:0 0 2px">
|
||||
<span style="flex:1">CODMAT</span>
|
||||
<span style="width:70px">Cant.</span>
|
||||
<span style="width:30px"></span>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="codmatLines">
|
||||
<!-- Dynamic CODMAT lines will be added here -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addCodmatLine()">
|
||||
<i class="bi bi-plus"></i> Adauga CODMAT
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="addCodmatLine()" style="font-size:0.8rem; padding:2px 10px">
|
||||
+ CODMAT
|
||||
</button>
|
||||
<div id="pctWarning" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
@@ -146,5 +150,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/mappings.js?v=9"></script>
|
||||
<script src="{{ request.scope.get('root_path', '') }}/static/js/mappings.js?v=10"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,39 +65,10 @@
|
||||
</div>
|
||||
<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">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Mapeaza SKU: <code id="mapSku"></code></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Produs web:</small> <strong id="mapProductName"></strong>
|
||||
</div>
|
||||
<div id="mapCodmatLines">
|
||||
<!-- Dynamic CODMAT lines -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="addMapCodmatLine()">
|
||||
<i class="bi bi-plus"></i> Adauga CODMAT
|
||||
</button>
|
||||
<div id="mapPctWarning" class="text-danger mt-2" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuleaza</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveQuickMap()">Salveaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentMapSku = '';
|
||||
let mapAcTimeout = null;
|
||||
let currentPage = 1;
|
||||
let skuStatusFilter = 'unresolved';
|
||||
let missingPerPage = 20;
|
||||
@@ -223,7 +194,7 @@ function renderMissingSkusTable(skus, data) {
|
||||
<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">
|
||||
? `<a href="#" class="btn-map-icon" onclick="event.stopPropagation(); openMapModal('${esc(s.sku)}', '${esc(s.product_name || '')}'); return false;" title="Mapeaza">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</a>`
|
||||
: `<small class="text-muted">${s.resolved_at ? new Date(s.resolved_at).toLocaleDateString('ro-RO') : ''}</small>`}
|
||||
@@ -234,7 +205,7 @@ function renderMissingSkusTable(skus, data) {
|
||||
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>`
|
||||
? `<a href="#" class="btn-map-icon" onclick="event.stopPropagation(); 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"`
|
||||
@@ -259,132 +230,14 @@ function renderPagination(data) {
|
||||
if (bot) bot.innerHTML = pagHtml;
|
||||
}
|
||||
|
||||
// ── Multi-CODMAT Map Modal ───────────────────────
|
||||
// ── Map Modal (uses shared openQuickMap) ─────────
|
||||
|
||||
function openMapModal(sku, productName) {
|
||||
currentMapSku = sku;
|
||||
document.getElementById('mapSku').textContent = sku;
|
||||
document.getElementById('mapProductName').textContent = productName || '-';
|
||||
document.getElementById('mapPctWarning').style.display = 'none';
|
||||
|
||||
const container = document.getElementById('mapCodmatLines');
|
||||
container.innerHTML = '';
|
||||
addMapCodmatLine();
|
||||
|
||||
new bootstrap.Modal(document.getElementById('mapModal')).show();
|
||||
}
|
||||
|
||||
function addMapCodmatLine() {
|
||||
const container = document.getElementById('mapCodmatLines');
|
||||
const idx = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border rounded p-2 mb-2 mc-line';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col position-relative">
|
||||
<input type="text" class="form-control form-control-sm mc-codmat" placeholder="Cauta CODMAT..." autocomplete="off">
|
||||
<div class="autocomplete-dropdown d-none mc-ac-dropdown"></div>
|
||||
<small class="text-muted mc-selected"></small>
|
||||
</div>
|
||||
<div class="col-auto" style="width:90px">
|
||||
<input type="number" class="form-control form-control-sm mc-cantitate" value="1" step="0.001" min="0.001" placeholder="Cant." title="Cantitate ROA">
|
||||
</div>
|
||||
<div class="col-auto" style="width:90px">
|
||||
<input type="number" class="form-control form-control-sm mc-procent" value="100" step="0.01" min="0" max="100" placeholder="% Pret" title="Procent Pret">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
${idx > 0 ? `<button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.mc-line').remove()"><i class="bi bi-x"></i></button>` : '<div style="width:31px"></div>'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
const input = div.querySelector('.mc-codmat');
|
||||
const dropdown = div.querySelector('.mc-ac-dropdown');
|
||||
const selected = div.querySelector('.mc-selected');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(mapAcTimeout);
|
||||
mapAcTimeout = setTimeout(() => mcAutocomplete(input, dropdown, selected), 250);
|
||||
openQuickMap({
|
||||
sku,
|
||||
productName,
|
||||
onSave: () => { loadMissingSkus(currentPage); }
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
async function mcAutocomplete(input, dropdown, selectedEl) {
|
||||
const q = input.value;
|
||||
if (q.length < 2) { dropdown.classList.add('d-none'); return; }
|
||||
try {
|
||||
const res = await fetch(`/api/articles/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="mcSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function mcSelectArticle(el, codmat, label) {
|
||||
const line = el.closest('.mc-line');
|
||||
line.querySelector('.mc-codmat').value = codmat;
|
||||
line.querySelector('.mc-selected').textContent = label;
|
||||
line.querySelector('.mc-ac-dropdown').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function saveQuickMap() {
|
||||
const lines = document.querySelectorAll('.mc-line');
|
||||
const mappings = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const codmat = line.querySelector('.mc-codmat').value.trim();
|
||||
const cantitate = parseFloat(line.querySelector('.mc-cantitate').value) || 1;
|
||||
const procent = parseFloat(line.querySelector('.mc-procent').value) || 100;
|
||||
if (!codmat) continue;
|
||||
mappings.push({ codmat, cantitate_roa: cantitate, procent_pret: procent });
|
||||
}
|
||||
|
||||
if (mappings.length === 0) { alert('Selecteaza cel putin un CODMAT'); return; }
|
||||
|
||||
if (mappings.length > 1) {
|
||||
const totalPct = mappings.reduce((s, m) => s + m.procent_pret, 0);
|
||||
if (Math.abs(totalPct - 100) > 0.01) {
|
||||
document.getElementById('mapPctWarning').textContent = `Suma procentelor trebuie sa fie 100% (actual: ${totalPct.toFixed(2)}%)`;
|
||||
document.getElementById('mapPctWarning').style.display = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
document.getElementById('mapPctWarning').style.display = 'none';
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (mappings.length === 1) {
|
||||
res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentMapSku, codmat: mappings[0].codmat, cantitate_roa: mappings[0].cantitate_roa, procent_pret: mappings[0].procent_pret })
|
||||
});
|
||||
} else {
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: currentMapSku, mappings })
|
||||
});
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('mapModal')).hide();
|
||||
loadMissingSkus(currentPage);
|
||||
} else {
|
||||
alert('Eroare: ' + (data.error || 'Unknown'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Eroare: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function exportMissingCsv() {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
-- separate_line: componentele se insereaza la pret plin +
|
||||
-- linii discount separate grupate pe cota TVA
|
||||
-- p_id_pol_productie — politica de pret pentru articole de productie
|
||||
-- (cont_vanzare in 341/345); NULL = nu se foloseste
|
||||
-- (cont in 341/345); NULL = nu se foloseste
|
||||
-- p_kit_discount_codmat — CODMAT-ul articolului discount (Mode separate_line)
|
||||
-- p_kit_discount_id_pol — id_pol pentru liniile discount (Mode separate_line)
|
||||
--
|
||||
@@ -349,7 +349,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
|
||||
|
||||
-- Determina id_pol_comp: cont 341/345 → politica productie, altfel vanzare
|
||||
BEGIN
|
||||
SELECT NVL(na.cont_vanzare, '') INTO v_cont_vanz
|
||||
SELECT NVL(na.cont, '') INTO v_cont_vanz
|
||||
FROM nom_articole na
|
||||
WHERE na.id_articol = v_kit_comps(v_comp_idx).id_articol
|
||||
AND ROWNUM = 1;
|
||||
|
||||
Reference in New Issue
Block a user