chore: commit all pending changes including deploy scripts and Windows config

- deploy.ps1, iis-web.config: Windows Server deployment scripts
- api/app/routers/sync.py, dashboard.py: router updates
- api/app/services/import_service.py, sync_service.py: service updates
- api/app/static/css/style.css, js/*.js: UI updates
- api/database-scripts/08_PACK_FACTURARE.pck: Oracle package
- .gitignore: add .gittoken
- CLAUDE.md, agent configs: documentation updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-16 15:05:04 +00:00
parent ee60a17f00
commit a9d0cead79
16 changed files with 17798 additions and 329 deletions

View File

@@ -15,3 +15,7 @@ async def dashboard(request: Request):
@router.get("/missing-skus", response_class=HTMLResponse)
async def missing_skus_page(request: Request):
return templates.TemplateResponse("missing_skus.html", {"request": request})
@router.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
return templates.TemplateResponse("settings.html", {"request": request})

View File

@@ -24,6 +24,11 @@ class AppSettingsUpdate(BaseModel):
transport_codmat: str = ""
transport_vat: str = "21"
discount_codmat: str = ""
transport_id_pol: str = ""
discount_vat: str = "21"
discount_id_pol: str = ""
id_pol: str = ""
id_sectie: str = ""
# API endpoints
@@ -332,11 +337,12 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
period_days=0 without dates means all time.
"""
is_uninvoiced_filter = (status == "UNINVOICED")
is_invoiced_filter = (status == "INVOICED")
# For UNINVOICED: fetch all IMPORTED orders, then filter post-invoice-check
fetch_status = "IMPORTED" if is_uninvoiced_filter else status
fetch_per_page = 10000 if is_uninvoiced_filter else per_page
fetch_page = 1 if is_uninvoiced_filter else page
# For UNINVOICED/INVOICED: fetch all IMPORTED orders, then filter post-invoice-check
fetch_status = "IMPORTED" if (is_uninvoiced_filter or is_invoiced_filter) else status
fetch_per_page = 10000 if (is_uninvoiced_filter or is_invoiced_filter) else per_page
fetch_page = 1 if (is_uninvoiced_filter or is_invoiced_filter) else page
result = await sqlite_service.get_orders(
page=fetch_page, per_page=fetch_per_page, search=search,
@@ -391,6 +397,8 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
1 for o in all_orders
if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")
))
imported_total = counts.get("imported_all") or counts.get("imported", 0)
counts["facturate"] = max(0, imported_total - counts["nefacturate"])
counts.setdefault("total", counts.get("imported", 0) + counts.get("skipped", 0) + counts.get("error", 0))
# For UNINVOICED filter: apply server-side filtering + pagination
@@ -403,6 +411,15 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
result["page"] = page
result["per_page"] = per_page
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
elif is_invoiced_filter:
filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and o.get("invoice")]
total = len(filtered)
offset = (page - 1) * per_page
result["orders"] = filtered[offset:offset + per_page]
result["total"] = total
result["page"] = page
result["per_page"] = per_page
result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0
# Reshape response
return {
@@ -445,6 +462,11 @@ async def get_app_settings():
"transport_codmat": settings.get("transport_codmat", ""),
"transport_vat": settings.get("transport_vat", "21"),
"discount_codmat": settings.get("discount_codmat", ""),
"transport_id_pol": settings.get("transport_id_pol", ""),
"discount_vat": settings.get("discount_vat", "19"),
"discount_id_pol": settings.get("discount_id_pol", ""),
"id_pol": settings.get("id_pol", ""),
"id_sectie": settings.get("id_sectie", ""),
}
@@ -454,4 +476,9 @@ async def update_app_settings(config: AppSettingsUpdate):
await sqlite_service.set_app_setting("transport_codmat", config.transport_codmat)
await sqlite_service.set_app_setting("transport_vat", config.transport_vat)
await sqlite_service.set_app_setting("discount_codmat", config.discount_codmat)
await sqlite_service.set_app_setting("transport_id_pol", config.transport_id_pol)
await sqlite_service.set_app_setting("discount_vat", config.discount_vat)
await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol)
await sqlite_service.set_app_setting("id_pol", config.id_pol)
await sqlite_service.set_app_setting("id_sectie", config.id_sectie)
return {"success": True}

View File

@@ -80,22 +80,29 @@ def build_articles_json(items, order=None, settings=None) -> str:
# Transport as article with quantity +1
if order.delivery_cost > 0 and transport_codmat:
articles.append({
article_dict = {
"sku": transport_codmat,
"quantity": "1",
"price": str(order.delivery_cost),
"vat": transport_vat,
"name": "Transport"
})
}
if settings.get("transport_id_pol"):
article_dict["id_pol"] = settings["transport_id_pol"]
articles.append(article_dict)
# Discount total with quantity -1 (positive price)
if order.discount_total > 0 and discount_codmat:
articles.append({
discount_vat = settings.get("discount_vat", "19")
article_dict = {
"sku": discount_codmat,
"quantity": "-1",
"price": str(order.discount_total),
"vat": "21",
"vat": discount_vat,
"name": "Discount"
})
}
if settings.get("discount_id_pol"):
article_dict["id_pol"] = settings["discount_id_pol"]
articles.append(article_dict)
return json.dumps(articles)

View File

@@ -232,8 +232,10 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
)
# Step 2d: Pre-validate prices for importable articles
id_pol = id_pol or settings.ID_POL
id_sectie = id_sectie or settings.ID_SECTIE
# Load app settings (for transport/discount CODMAT config AND id_pol/id_sectie override)
app_settings = await sqlite_service.get_app_settings()
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
if id_pol and (truly_importable or already_in_roa):
@@ -331,9 +333,6 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
imported_count = 0
error_count = 0
# Load app settings for transport/discount CODMAT config
app_settings = await sqlite_service.get_app_settings()
for i, order in enumerate(truly_importable):
shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)

View File

@@ -100,6 +100,9 @@ body {
padding-right: 1.5rem;
padding-bottom: 1.5rem;
min-height: 100vh;
max-width: 1280px;
margin-left: auto;
margin-right: auto;
}
/* ── Cards ───────────────────────────────────────── */
@@ -140,6 +143,10 @@ body {
font-size: 1rem;
}
/* Zebra striping */
.table tbody tr:nth-child(even) td { background-color: #f7f8fa; }
.table-hover tbody tr:hover td { background-color: #eef2ff !important; }
/* ── Badges — soft pill style ────────────────────── */
.badge {
font-size: 0.8125rem;
@@ -736,3 +743,22 @@ tr.mapping-deleted td {
gap: 1rem;
color: #374151;
}
/* Clickable CODMAT link in order detail modal */
.codmat-link { color: #0d6efd; cursor: pointer; text-decoration: underline; }
.codmat-link:hover { color: #0a58ca; }
/* Mobile article flat list in order detail modal */
.detail-item-flat { font-size: 0.85rem; }
.detail-item-flat .dif-item { }
.detail-item-flat .dif-item:nth-child(even) .dif-row { background: #f7f8fa; }
.detail-item-flat .dif-row {
display: flex; align-items: baseline; gap: 0.5rem;
padding: 0.2rem 0.75rem; flex-wrap: wrap;
}
.dif-sku { font-family: monospace; font-size: 0.78rem; color: #6b7280; }
.dif-name { font-weight: 500; flex: 1; }
.dif-qty { white-space: nowrap; color: #6b7280; }
.dif-val { white-space: nowrap; font-weight: 600; }
.dif-codmat-link { color: #0d6efd; cursor: pointer; font-size: 0.78rem; font-family: monospace; }
.dif-codmat-link:hover { color: #0a58ca; text-decoration: underline; }

View File

@@ -281,42 +281,29 @@ async function loadDashOrders() {
if (el('cntImp')) el('cntImp').textContent = c.imported_all || c.imported || 0;
if (el('cntSkip')) el('cntSkip').textContent = c.skipped || 0;
if (el('cntErr')) el('cntErr').textContent = c.error || c.errors || 0;
if (el('cntNef')) el('cntNef').textContent = c.uninvoiced || c.nefacturate || 0;
if (el('cntFact')) el('cntFact').textContent = c.facturate || 0;
if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0;
const tbody = document.getElementById('dashOrdersBody');
const orders = data.orders || [];
if (orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted py-3">Nicio comanda</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted py-3">Nicio comanda</td></tr>';
} else {
tbody.innerHTML = orders.map(o => {
const dateStr = fmtDate(o.order_date);
const statusBadge = orderStatusBadge(o.status);
// Invoice info
let invoiceBadge = '';
if (o.status !== 'IMPORTED' && o.status !== 'ALREADY_IMPORTED') {
invoiceBadge = '<span class="text-muted">-</span>';
} else if (o.invoice && o.invoice.facturat) {
invoiceBadge = `<span style="color:#16a34a;font-weight:500">Facturat</span>`;
if (o.invoice.serie_act || o.invoice.numar_act) {
invoiceBadge += `<br><small>${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}</small>`;
}
} else {
invoiceBadge = `<span style="color:#dc2626">Nefacturat</span>`;
}
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
return `<tr style="cursor:pointer" onclick="openDashOrderDetail('${esc(o.order_number)}')">
<td><code>${esc(o.order_number)}</code></td>
<td>${dateStr}</td>
<td>${statusDot(o.status)}</td>
<td class="text-nowrap">${dateStr}</td>
${renderClientCell(o)}
<td><code>${esc(o.order_number)}</code></td>
<td>${o.items_count || 0}</td>
<td class="text-end">${orderTotal}</td>
<td class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td>
<td>${o.id_comanda || '-'}</td>
<td>${invoiceBadge}</td>
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
<td class="text-end fw-bold">${orderTotal}</td>
<td class="text-center">${invoiceDot(o)}</td>
</tr>`;
}).join('');
}
@@ -339,8 +326,8 @@ async function loadDashOrders() {
return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)}
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
<span class="grow truncate">${esc(name)}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}</span>
<span class="grow truncate fw-bold">${esc(name)}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · <strong>' + totalStr + '</strong>' : ''}</span>
</div>`;
}).join('');
}
@@ -352,7 +339,8 @@ async function loadDashOrders() {
{ 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' }
{ label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' },
{ label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' }
], (val) => {
document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);
@@ -380,7 +368,7 @@ async function loadDashOrders() {
});
} catch (err) {
document.getElementById('dashOrdersBody').innerHTML =
`<tr><td colspan="8" class="text-center text-danger">${esc(err.message)}</td></tr>`;
`<tr><td colspan="9" class="text-center text-danger">${esc(err.message)}</td></tr>`;
}
}
@@ -402,9 +390,9 @@ function renderClientCell(order) {
const billing = (order.billing_name || '').trim();
const isDiff = order.is_different_person && billing && shipping !== billing;
if (isDiff) {
return `<td class="tooltip-cont" data-tooltip="Cont: ${escHtml(billing)}">${escHtml(shipping)}&nbsp;<sup style="color:#6b7280;font-size:0.65rem">&#9650;</sup></td>`;
return `<td class="tooltip-cont fw-bold" data-tooltip="Cont: ${escHtml(billing)}">${escHtml(shipping)}&nbsp;<sup style="color:#6b7280;font-size:0.65rem">&#9650;</sup></td>`;
}
return `<td>${escHtml(shipping || billing || '\u2014')}</td>`;
return `<td class="fw-bold">${escHtml(shipping || billing || '\u2014')}</td>`;
}
// ── Helper functions ──────────────────────────────
@@ -428,6 +416,10 @@ function escHtml(s) {
// Alias kept for backward compat with inline handlers in modal
function esc(s) { return escHtml(s); }
function fmtCost(v) {
return v > 0 ? Number(v).toFixed(2) : '';
}
function statusLabelText(status) {
switch ((status || '').toUpperCase()) {
@@ -449,6 +441,12 @@ function orderStatusBadge(status) {
}
}
function invoiceDot(order) {
if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '';
if (order.invoice && order.invoice.facturat) return '<span class="dot dot-green" title="Facturat"></span>';
return '<span class="dot dot-red" title="Nefacturat"></span>';
}
function renderCodmatCell(item) {
if (!item.codmat_details || item.codmat_details.length === 0) {
return `<code>${esc(item.codmat || '-')}</code>`;
@@ -473,16 +471,12 @@ async function openDashOrderDetail(orderNumber) {
document.getElementById('detailIdPartener').textContent = '-';
document.getElementById('detailIdAdresaFact').textContent = '-';
document.getElementById('detailIdAdresaLivr').textContent = '-';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailError').style.display = 'none';
const detailItemsTotal = document.getElementById('detailItemsTotal');
if (detailItemsTotal) detailItemsTotal.textContent = '-';
const detailOrderTotal = document.getElementById('detailOrderTotal');
if (detailOrderTotal) detailOrderTotal.textContent = '-';
const deliveryWrap = document.getElementById('detailDeliveryWrap');
if (deliveryWrap) deliveryWrap.style.display = 'none';
const discountWrap = document.getElementById('detailDiscountWrap');
if (discountWrap) discountWrap.style.display = 'none';
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = '';
@@ -514,25 +508,15 @@ async function openDashOrderDetail(orderNumber) {
document.getElementById('detailError').style.display = '';
}
// Show delivery cost
const dlvWrap = document.getElementById('detailDeliveryWrap');
const dlvEl = document.getElementById('detailDeliveryCost');
if (order.delivery_cost && Number(order.delivery_cost) > 0) {
if (dlvEl) dlvEl.textContent = Number(order.delivery_cost).toFixed(2) + ' lei';
if (dlvWrap) dlvWrap.style.display = '';
}
if (dlvEl) dlvEl.textContent = order.delivery_cost > 0 ? Number(order.delivery_cost).toFixed(2) + ' lei' : '';
// Show discount
const dscWrap = document.getElementById('detailDiscountWrap');
const dscEl = document.getElementById('detailDiscount');
if (order.discount_total && Number(order.discount_total) > 0) {
if (dscEl) dscEl.textContent = '-' + Number(order.discount_total).toFixed(2) + ' lei';
if (dscWrap) dscWrap.style.display = '';
}
if (dscEl) dscEl.textContent = order.discount_total > 0 ? '' + Number(order.discount_total).toFixed(2) + ' lei' : '';
const items = data.items || [];
if (items.length === 0) {
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center text-muted">Niciun articol</td></tr>';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center text-muted">Niciun articol</td></tr>';
return;
}
@@ -541,53 +525,38 @@ async function openDashOrderDetail(orderNumber) {
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
// Mobile article cards
// Mobile article flat list
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) {
mobileContainer.innerHTML = items.map(item => {
let statusLabel = '';
switch (item.mapping_status) {
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
default: statusLabel = '<span class="badge bg-secondary">?</span>';
}
const codmat = item.codmat || '-';
return `<div class="detail-item-card">
<div class="card-sku">${esc(item.sku)}</div>
<div class="card-name">${esc(item.product_name || '-')}</div>
<div class="card-details">
<span>x${item.quantity || 0}</span>
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
<span><code>${esc(codmat)}</code></span>
<span>${statusLabel}</span>
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>`;
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
return `<div class="dif-item">
<div class="dif-row">
<span class="dif-sku">${esc(item.sku)}</span>
${codmatList}
</div>
<div class="dif-row">
<span class="dif-name">${esc(item.product_name || '')}</span>
<span class="dif-qty">x${item.quantity || 0}</span>
<span class="dif-val">${valoare} lei</span>
</div>
</div>`;
}).join('');
}).join('') + '</div>';
}
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
let statusBadge;
switch (item.mapping_status) {
case 'mapped': statusBadge = '<span class="badge bg-success">Mapat</span>'; break;
case 'direct': statusBadge = '<span class="badge bg-info">Direct</span>'; break;
case 'missing': statusBadge = '<span class="badge bg-warning">Lipsa</span>'; break;
default: statusBadge = '<span class="badge bg-secondary">?</span>';
}
const action = item.mapping_status === 'missing'
? `<a href="#" class="btn-map-icon" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}'); return false;" title="Mapeaza"><i class="bi bi-link-45deg"></i></a>`
: '';
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>`;
return `<tr>
<td><code>${esc(item.sku)}</code></td>
<td>${esc(item.product_name || '-')}</td>
<td>${codmatCell}</td>
<td>${item.quantity || 0}</td>
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
<td>${item.vat != null ? Number(item.vat).toFixed(2) : '-'}</td>
<td>${renderCodmatCell(item)}</td>
<td>${statusBadge}</td>
<td>${action}</td>
<td class="text-end">${valoare}</td>
</tr>`;
}).join('');
} catch (err) {
@@ -730,80 +699,3 @@ async function saveQuickMapping() {
}
}
// ── App Settings ─────────────────────────────────
let settAcTimeout = null;
document.addEventListener('DOMContentLoaded', () => {
loadAppSettings();
wireSettingsAutocomplete('settTransportCodmat', 'settTransportAc');
wireSettingsAutocomplete('settDiscountCodmat', 'settDiscountAc');
});
async function loadAppSettings() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
const el = (id) => document.getElementById(id);
if (el('settTransportCodmat')) el('settTransportCodmat').value = data.transport_codmat || '';
if (el('settTransportVat')) el('settTransportVat').value = data.transport_vat || '21';
if (el('settDiscountCodmat')) el('settDiscountCodmat').value = data.discount_codmat || '';
} catch (err) {
console.error('loadAppSettings error:', err);
}
}
async function saveAppSettings() {
const transport_codmat = document.getElementById('settTransportCodmat')?.value?.trim() || '';
const transport_vat = document.getElementById('settTransportVat')?.value || '21';
const discount_codmat = document.getElementById('settDiscountCodmat')?.value?.trim() || '';
try {
const res = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transport_codmat, transport_vat, discount_codmat })
});
const data = await res.json();
if (data.success) {
alert('Setari salvate!');
} else {
alert('Eroare: ' + JSON.stringify(data));
}
} catch (err) {
alert('Eroare salvare setari: ' + err.message);
}
}
function wireSettingsAutocomplete(inputId, dropdownId) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
if (!input || !dropdown) return;
input.addEventListener('input', () => {
clearTimeout(settAcTimeout);
settAcTimeout = setTimeout(async () => {
const q = input.value.trim();
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="settSelectArticle('${inputId}', '${dropdownId}', '${esc(r.codmat)}')">
<span class="codmat">${esc(r.codmat)}</span> &mdash; <span class="denumire">${esc(r.denumire)}</span>
</div>`
).join('');
dropdown.classList.remove('d-none');
} catch { dropdown.classList.add('d-none'); }
}, 250);
});
input.addEventListener('blur', () => {
setTimeout(() => dropdown.classList.add('d-none'), 200);
});
}
function settSelectArticle(inputId, dropdownId, codmat) {
document.getElementById(inputId).value = codmat;
document.getElementById(dropdownId).classList.add('d-none');
}

View File

@@ -10,6 +10,10 @@ let currentQmOrderNumber = '';
let ordersSortColumn = 'order_date';
let ordersSortDirection = 'desc';
function fmtCost(v) {
return v > 0 ? Number(v).toFixed(2) : '';
}
function fmtDuration(startedAt, finishedAt) {
if (!startedAt || !finishedAt) return '-';
const diffMs = new Date(finishedAt) - new Date(startedAt);
@@ -151,19 +155,21 @@ async function loadRunOrders(runId, statusFilter, page) {
const orders = data.orders || [];
if (orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">Nicio comanda</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted py-3">Nicio comanda</td></tr>';
} else {
tbody.innerHTML = orders.map((o, i) => {
const dateStr = fmtDate(o.order_date);
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
return `<tr style="cursor:pointer" onclick="openOrderDetail('${esc(o.order_number)}')">
<td>${statusDot(o.status)}</td>
<td>${(ordersPage - 1) * 50 + i + 1}</td>
<td>${dateStr}</td>
<td class="text-nowrap">${dateStr}</td>
<td><code>${esc(o.order_number)}</code></td>
<td>${esc(o.customer_name)}</td>
<td class="fw-bold">${esc(o.customer_name)}</td>
<td>${o.items_count || 0}</td>
<td class="text-end">${orderTotal}</td>
<td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
<td class="text-end fw-bold">${orderTotal}</td>
</tr>`;
}).join('');
}
@@ -185,8 +191,8 @@ async function loadRunOrders(runId, statusFilter, page) {
return `<div class="flat-row" onclick="openOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)}
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
<span class="grow truncate">${esc(o.customer_name || '—')}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + totalStr : ''}</span>
<span class="grow truncate fw-bold">${esc(o.customer_name || '—')}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · <strong>' + totalStr + '</strong>' : ''}</span>
</div>`;
}).join('');
}
@@ -222,7 +228,7 @@ async function loadRunOrders(runId, statusFilter, page) {
}
} catch (err) {
document.getElementById('runOrdersBody').innerHTML =
`<tr><td colspan="6" class="text-center text-danger">${esc(err.message)}</td></tr>`;
`<tr><td colspan="9" class="text-center text-danger">${esc(err.message)}</td></tr>`;
}
}
@@ -318,16 +324,12 @@ async function openOrderDetail(orderNumber) {
document.getElementById('detailIdPartener').textContent = '-';
document.getElementById('detailIdAdresaFact').textContent = '-';
document.getElementById('detailIdAdresaLivr').textContent = '-';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailError').style.display = 'none';
const detailItemsTotal = document.getElementById('detailItemsTotal');
if (detailItemsTotal) detailItemsTotal.textContent = '-';
const detailOrderTotal = document.getElementById('detailOrderTotal');
if (detailOrderTotal) detailOrderTotal.textContent = '-';
const deliveryWrap = document.getElementById('detailDeliveryWrap');
if (deliveryWrap) deliveryWrap.style.display = 'none';
const discountWrap = document.getElementById('detailDiscountWrap');
if (discountWrap) discountWrap.style.display = 'none';
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = '';
@@ -359,25 +361,15 @@ async function openOrderDetail(orderNumber) {
document.getElementById('detailError').style.display = '';
}
// Show delivery cost
const dlvWrap = document.getElementById('detailDeliveryWrap');
const dlvEl = document.getElementById('detailDeliveryCost');
if (order.delivery_cost && Number(order.delivery_cost) > 0) {
if (dlvEl) dlvEl.textContent = Number(order.delivery_cost).toFixed(2) + ' lei';
if (dlvWrap) dlvWrap.style.display = '';
}
if (dlvEl) dlvEl.textContent = order.delivery_cost > 0 ? Number(order.delivery_cost).toFixed(2) + ' lei' : '';
// Show discount
const dscWrap = document.getElementById('detailDiscountWrap');
const dscEl = document.getElementById('detailDiscount');
if (order.discount_total && Number(order.discount_total) > 0) {
if (dscEl) dscEl.textContent = '-' + Number(order.discount_total).toFixed(2) + ' lei';
if (dscWrap) dscWrap.style.display = '';
}
if (dscEl) dscEl.textContent = order.discount_total > 0 ? '' + Number(order.discount_total).toFixed(2) + ' lei' : '';
const items = data.items || [];
if (items.length === 0) {
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center text-muted">Niciun articol</td></tr>';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="6" class="text-center text-muted">Niciun articol</td></tr>';
return;
}
@@ -386,53 +378,38 @@ async function openOrderDetail(orderNumber) {
document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei';
document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-';
// Mobile article cards
// Mobile article flat list
const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) {
mobileContainer.innerHTML = items.map(item => {
let statusLabel = '';
switch (item.mapping_status) {
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
default: statusLabel = '<span class="badge bg-secondary">?</span>';
}
const codmat = item.codmat || '-';
return `<div class="detail-item-card">
<div class="card-sku">${esc(item.sku)}</div>
<div class="card-name">${esc(item.product_name || '-')}</div>
<div class="card-details">
<span>x${item.quantity || 0}</span>
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
<span><code>${esc(codmat)}</code></span>
<span>${statusLabel}</span>
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>`;
const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2);
return `<div class="dif-item">
<div class="dif-row">
<span class="dif-sku">${esc(item.sku)}</span>
${codmatList}
</div>
<div class="dif-row">
<span class="dif-name">${esc(item.product_name || '')}</span>
<span class="dif-qty">x${item.quantity || 0}</span>
<span class="dif-val">${valoare} lei</span>
</div>
</div>`;
}).join('');
}).join('') + '</div>';
}
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
let statusBadge;
switch (item.mapping_status) {
case 'mapped': statusBadge = '<span class="badge bg-success">Mapat</span>'; break;
case 'direct': statusBadge = '<span class="badge bg-info">Direct</span>'; break;
case 'missing': statusBadge = '<span class="badge bg-warning">Lipsa</span>'; break;
default: statusBadge = '<span class="badge bg-secondary">?</span>';
}
const action = item.mapping_status === 'missing'
? `<a href="#" class="btn-map-icon" onclick="openQuickMap('${esc(item.sku)}', '${esc(item.product_name || '')}', '${esc(orderNumber)}'); return false;" title="Mapeaza"><i class="bi bi-link-45deg"></i></a>`
: '';
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>`;
return `<tr>
<td><code>${esc(item.sku)}</code></td>
<td>${esc(item.product_name || '-')}</td>
<td>${codmatCell}</td>
<td>${item.quantity || 0}</td>
<td>${item.price != null ? Number(item.price).toFixed(2) : '-'}</td>
<td>${item.vat != null ? Number(item.vat).toFixed(2) : '-'}</td>
<td>${renderCodmatCell(item)}</td>
<td>${statusBadge}</td>
<td>${action}</td>
<td class="text-end">${valoare}</td>
</tr>`;
}).join('');
} catch (err) {

View File

@@ -0,0 +1,101 @@
let settAcTimeout = null;
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
wireAutocomplete('settTransportCodmat', 'settTransportAc');
wireAutocomplete('settDiscountCodmat', 'settDiscountAc');
});
async function loadSettings() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
const el = (id) => document.getElementById(id);
if (el('settTransportCodmat')) el('settTransportCodmat').value = data.transport_codmat || '';
if (el('settTransportVat')) el('settTransportVat').value = data.transport_vat || '21';
if (el('settTransportIdPol')) el('settTransportIdPol').value = data.transport_id_pol || '';
if (el('settDiscountCodmat')) el('settDiscountCodmat').value = data.discount_codmat || '';
if (el('settDiscountVat')) el('settDiscountVat').value = data.discount_vat || '19';
if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || '';
if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
} catch (err) {
console.error('loadSettings error:', err);
}
}
async function saveSettings() {
const el = (id) => document.getElementById(id);
const payload = {
transport_codmat: el('settTransportCodmat')?.value?.trim() || '',
transport_vat: el('settTransportVat')?.value || '21',
transport_id_pol: el('settTransportIdPol')?.value?.trim() || '',
discount_codmat: el('settDiscountCodmat')?.value?.trim() || '',
discount_vat: el('settDiscountVat')?.value || '19',
discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '',
id_pol: el('settIdPol')?.value?.trim() || '',
id_sectie: el('settIdSectie')?.value?.trim() || '',
};
try {
const res = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
const resultEl = document.getElementById('settSaveResult');
if (data.success) {
if (resultEl) { resultEl.textContent = 'Salvat!'; resultEl.style.color = '#16a34a'; }
setTimeout(() => { if (resultEl) resultEl.textContent = ''; }, 3000);
} else {
if (resultEl) { resultEl.textContent = 'Eroare: ' + JSON.stringify(data); resultEl.style.color = '#dc2626'; }
}
} catch (err) {
const resultEl = document.getElementById('settSaveResult');
if (resultEl) { resultEl.textContent = 'Eroare: ' + err.message; resultEl.style.color = '#dc2626'; }
}
}
function wireAutocomplete(inputId, dropdownId) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
if (!input || !dropdown) return;
input.addEventListener('input', () => {
clearTimeout(settAcTimeout);
settAcTimeout = setTimeout(async () => {
const q = input.value.trim();
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="settSelectArticle('${inputId}', '${dropdownId}', '${escHtml(r.codmat)}')">
<span class="codmat">${escHtml(r.codmat)}</span> &mdash; <span class="denumire">${escHtml(r.denumire)}</span>
</div>`
).join('');
dropdown.classList.remove('d-none');
} catch { dropdown.classList.add('d-none'); }
}, 250);
});
input.addEventListener('blur', () => {
setTimeout(() => dropdown.classList.add('d-none'), 200);
});
}
function settSelectArticle(inputId, dropdownId, codmat) {
document.getElementById(inputId).value = codmat;
document.getElementById(dropdownId).classList.add('d-none');
}
function escHtml(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}