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:
@@ -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})
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)} <sup style="color:#6b7280;font-size:0.65rem">▲</sup></td>`;
|
||||
return `<td class="tooltip-cont fw-bold" data-tooltip="Cont: ${escHtml(billing)}">${escHtml(shipping)} <sup style="color:#6b7280;font-size:0.65rem">▲</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> — <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');
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
101
api/app/static/js/settings.js
Normal file
101
api/app/static/js/settings.js
Normal 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> — <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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
Reference in New Issue
Block a user