feat(sync): add delivery cost, discount tracking and import settings

Parse delivery.total and discounts[] from GoMag JSON into new
delivery_cost/discount_total fields. Add app_settings table for
configuring transport/discount CODMAT codes. When configured,
transport and discount are appended as extra articles in the
Oracle import JSON. Reorder Total column in dashboard/logs tables
and show transport/discount breakdown in order detail modals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-16 10:15:17 +00:00
parent 137c4a8b0b
commit 25aa9e544c
10 changed files with 302 additions and 22 deletions

View File

@@ -104,7 +104,9 @@ CREATE TABLE IF NOT EXISTS orders (
factura_total_tva REAL, factura_total_tva REAL,
factura_total_cu_tva REAL, factura_total_cu_tva REAL,
invoice_checked_at TEXT, invoice_checked_at TEXT,
order_total REAL order_total REAL,
delivery_cost REAL,
discount_total REAL
); );
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status); CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date); CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date);
@@ -140,6 +142,11 @@ CREATE TABLE IF NOT EXISTS web_products (
order_count INTEGER DEFAULT 0 order_count INTEGER DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value TEXT
);
CREATE TABLE IF NOT EXISTS order_items ( CREATE TABLE IF NOT EXISTS order_items (
order_number TEXT, order_number TEXT,
sku TEXT, sku TEXT,
@@ -303,6 +310,8 @@ def init_sqlite():
("factura_total_cu_tva", "REAL"), ("factura_total_cu_tva", "REAL"),
("invoice_checked_at", "TEXT"), ("invoice_checked_at", "TEXT"),
("order_total", "REAL"), ("order_total", "REAL"),
("delivery_cost", "REAL"),
("discount_total", "REAL"),
]: ]:
if col not in order_cols: if col not in order_cols:
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}") conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")

View File

@@ -20,6 +20,12 @@ class ScheduleConfig(BaseModel):
interval_minutes: int = 5 interval_minutes: int = 5
class AppSettingsUpdate(BaseModel):
transport_codmat: str = ""
transport_vat: str = "21"
discount_codmat: str = ""
# API endpoints # API endpoints
@router.post("/api/sync/start") @router.post("/api/sync/start")
async def start_sync(background_tasks: BackgroundTasks): async def start_sync(background_tasks: BackgroundTasks):
@@ -429,3 +435,23 @@ async def update_schedule(config: ScheduleConfig):
async def get_schedule(): async def get_schedule():
"""Get current scheduler status.""" """Get current scheduler status."""
return scheduler_service.get_scheduler_status() return scheduler_service.get_scheduler_status()
@router.get("/api/settings")
async def get_app_settings():
"""Get application settings."""
settings = await sqlite_service.get_app_settings()
return {
"transport_codmat": settings.get("transport_codmat", ""),
"transport_vat": settings.get("transport_vat", "21"),
"discount_codmat": settings.get("discount_codmat", ""),
}
@router.put("/api/settings")
async def update_app_settings(config: AppSettingsUpdate):
"""Update application settings."""
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)
return {"success": True}

View File

@@ -60,8 +60,9 @@ def format_address_for_oracle(address: str, city: str, region: str) -> str:
return f"JUD:{region_clean};{city_clean};{address_clean}" return f"JUD:{region_clean};{city_clean};{address_clean}"
def build_articles_json(items) -> str: def build_articles_json(items, order=None, settings=None) -> str:
"""Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda.""" """Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda.
Includes transport and discount as extra articles if configured."""
articles = [] articles = []
for item in items: for item in items:
articles.append({ articles.append({
@@ -71,10 +72,35 @@ def build_articles_json(items) -> str:
"vat": str(item.vat), "vat": str(item.vat),
"name": clean_web_text(item.name) "name": clean_web_text(item.name)
}) })
if order and settings:
transport_codmat = settings.get("transport_codmat", "")
transport_vat = settings.get("transport_vat", "21")
discount_codmat = settings.get("discount_codmat", "")
# Transport as article with quantity +1
if order.delivery_cost > 0 and transport_codmat:
articles.append({
"sku": transport_codmat,
"quantity": "1",
"price": str(order.delivery_cost),
"vat": transport_vat,
"name": "Transport"
})
# Discount total with quantity -1 (positive price)
if order.discount_total > 0 and discount_codmat:
articles.append({
"sku": discount_codmat,
"quantity": "-1",
"price": str(order.discount_total),
"vat": "21",
"name": "Discount"
})
return json.dumps(articles) return json.dumps(articles)
def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dict: def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None) -> dict:
"""Import a single order into Oracle ROA. """Import a single order into Oracle ROA.
Returns dict with: Returns dict with:
@@ -203,7 +229,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dic
result["id_adresa_livrare"] = int(addr_livr_id) result["id_adresa_livrare"] = int(addr_livr_id)
# Step 4: Build articles JSON and import order # Step 4: Build articles JSON and import order
articles_json = build_articles_json(order.items) articles_json = build_articles_json(order.items, order, app_settings)
# Use CLOB for the JSON # Use CLOB for the JSON
clob_var = cur.var(oracledb.DB_TYPE_CLOB) clob_var = cur.var(oracledb.DB_TYPE_CLOB)

View File

@@ -55,6 +55,8 @@ class OrderData:
billing: OrderBilling = field(default_factory=OrderBilling) billing: OrderBilling = field(default_factory=OrderBilling)
shipping: Optional[OrderShipping] = None shipping: Optional[OrderShipping] = None
total: float = 0.0 total: float = 0.0
delivery_cost: float = 0.0
discount_total: float = 0.0
payment_name: str = "" payment_name: str = ""
delivery_name: str = "" delivery_name: str = ""
source_file: str = "" source_file: str = ""
@@ -155,6 +157,15 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
payment = data.get("payment", {}) or {} payment = data.get("payment", {}) or {}
delivery = data.get("delivery", {}) or {} delivery = data.get("delivery", {}) or {}
# Parse delivery cost
delivery_cost = float(delivery.get("total", 0) or 0) if isinstance(delivery, dict) else 0.0
# Parse discount total (sum of all discount values)
discount_total = 0.0
for d in data.get("discounts", []):
if isinstance(d, dict):
discount_total += float(d.get("value", 0) or 0)
return OrderData( return OrderData(
id=str(data.get("id", order_id)), id=str(data.get("id", order_id)),
number=str(data.get("number", "")), number=str(data.get("number", "")),
@@ -165,6 +176,8 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData:
billing=billing, billing=billing,
shipping=shipping, shipping=shipping,
total=float(data.get("total", 0) or 0), total=float(data.get("total", 0) or 0),
delivery_cost=delivery_cost,
discount_total=discount_total,
payment_name=str(payment.get("name", "")) if isinstance(payment, dict) else "", payment_name=str(payment.get("name", "")) if isinstance(payment, dict) else "",
delivery_name=str(delivery.get("name", "")) if isinstance(delivery, dict) else "", delivery_name=str(delivery.get("name", "")) if isinstance(delivery, dict) else "",
source_file=source_file source_file=source_file

View File

@@ -51,7 +51,8 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
missing_skus: list = None, items_count: int = 0, missing_skus: list = None, items_count: int = 0,
shipping_name: str = None, billing_name: str = None, shipping_name: str = None, billing_name: str = None,
payment_method: str = None, delivery_method: str = None, payment_method: str = None, delivery_method: str = None,
order_total: float = None): order_total: float = None,
delivery_cost: float = None, discount_total: float = None):
"""Upsert a single order — one row per order_number, status updated in place.""" """Upsert a single order — one row per order_number, status updated in place."""
db = await get_sqlite() db = await get_sqlite()
try: try:
@@ -60,8 +61,9 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
(order_number, order_date, customer_name, status, (order_number, order_date, customer_name, status,
id_comanda, id_partener, error_message, missing_skus, items_count, id_comanda, id_partener, error_message, missing_skus, items_count,
last_sync_run_id, shipping_name, billing_name, last_sync_run_id, shipping_name, billing_name,
payment_method, delivery_method, order_total) payment_method, delivery_method, order_total,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) delivery_cost, discount_total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(order_number) DO UPDATE SET ON CONFLICT(order_number) DO UPDATE SET
status = CASE status = CASE
WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED' WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED'
@@ -82,12 +84,15 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
payment_method = COALESCE(excluded.payment_method, orders.payment_method), payment_method = COALESCE(excluded.payment_method, orders.payment_method),
delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method), delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method),
order_total = COALESCE(excluded.order_total, orders.order_total), order_total = COALESCE(excluded.order_total, orders.order_total),
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
updated_at = datetime('now') updated_at = datetime('now')
""", (order_number, order_date, customer_name, status, """, (order_number, order_date, customer_name, status,
id_comanda, id_partener, error_message, id_comanda, id_partener, error_message,
json.dumps(missing_skus) if missing_skus else None, json.dumps(missing_skus) if missing_skus else None,
items_count, sync_run_id, shipping_name, billing_name, items_count, sync_run_id, shipping_name, billing_name,
payment_method, delivery_method, order_total)) payment_method, delivery_method, order_total,
delivery_cost, discount_total))
await db.commit() await db.commit()
finally: finally:
await db.close() await db.close()
@@ -112,7 +117,7 @@ async def save_orders_batch(orders_data: list[dict]):
Each dict must have: sync_run_id, order_number, order_date, customer_name, status, Each dict must have: sync_run_id, order_number, order_date, customer_name, status,
id_comanda, id_partener, error_message, missing_skus (list|None), items_count, id_comanda, id_partener, error_message, missing_skus (list|None), items_count,
shipping_name, billing_name, payment_method, delivery_method, status_at_run, shipping_name, billing_name, payment_method, delivery_method, status_at_run,
items (list of item dicts). items (list of item dicts), delivery_cost (optional), discount_total (optional).
""" """
if not orders_data: if not orders_data:
return return
@@ -124,8 +129,9 @@ async def save_orders_batch(orders_data: list[dict]):
(order_number, order_date, customer_name, status, (order_number, order_date, customer_name, status,
id_comanda, id_partener, error_message, missing_skus, items_count, id_comanda, id_partener, error_message, missing_skus, items_count,
last_sync_run_id, shipping_name, billing_name, last_sync_run_id, shipping_name, billing_name,
payment_method, delivery_method, order_total) payment_method, delivery_method, order_total,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) delivery_cost, discount_total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(order_number) DO UPDATE SET ON CONFLICT(order_number) DO UPDATE SET
status = CASE status = CASE
WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED' WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED'
@@ -146,6 +152,8 @@ async def save_orders_batch(orders_data: list[dict]):
payment_method = COALESCE(excluded.payment_method, orders.payment_method), payment_method = COALESCE(excluded.payment_method, orders.payment_method),
delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method), delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method),
order_total = COALESCE(excluded.order_total, orders.order_total), order_total = COALESCE(excluded.order_total, orders.order_total),
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
updated_at = datetime('now') updated_at = datetime('now')
""", [ """, [
(d["order_number"], d["order_date"], d["customer_name"], d["status"], (d["order_number"], d["order_date"], d["customer_name"], d["status"],
@@ -154,7 +162,8 @@ async def save_orders_batch(orders_data: list[dict]):
d.get("items_count", 0), d["sync_run_id"], d.get("items_count", 0), d["sync_run_id"],
d.get("shipping_name"), d.get("billing_name"), d.get("shipping_name"), d.get("billing_name"),
d.get("payment_method"), d.get("delivery_method"), d.get("payment_method"), d.get("delivery_method"),
d.get("order_total")) d.get("order_total"),
d.get("delivery_cost"), d.get("discount_total"))
for d in orders_data for d in orders_data
]) ])
@@ -768,3 +777,29 @@ async def update_order_invoice(order_number: str, serie: str = None,
await db.commit() await db.commit()
finally: finally:
await db.close() await db.close()
# ── App Settings ─────────────────────────────────
async def get_app_settings() -> dict:
"""Get all app settings as a dict."""
db = await get_sqlite()
try:
cursor = await db.execute("SELECT key, value FROM app_settings")
rows = await cursor.fetchall()
return {row["key"]: row["value"] for row in rows}
finally:
await db.close()
async def set_app_setting(key: str, value: str):
"""Set a single app setting value."""
db = await get_sqlite()
try:
await db.execute("""
INSERT OR REPLACE INTO app_settings (key, value)
VALUES (?, ?)
""", (key, value))
await db.commit()
finally:
await db.close()

View File

@@ -287,6 +287,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
"shipping_name": shipping_name, "billing_name": billing_name, "shipping_name": shipping_name, "billing_name": billing_name,
"payment_method": payment_method, "delivery_method": delivery_method, "payment_method": payment_method, "delivery_method": delivery_method,
"order_total": order.total or None, "order_total": order.total or None,
"delivery_cost": order.delivery_cost or None,
"discount_total": order.discount_total or None,
"items": order_items_data, "items": order_items_data,
}) })
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})") _log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})")
@@ -315,6 +317,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
"shipping_name": shipping_name, "billing_name": billing_name, "shipping_name": shipping_name, "billing_name": billing_name,
"payment_method": payment_method, "delivery_method": delivery_method, "payment_method": payment_method, "delivery_method": delivery_method,
"order_total": order.total or None, "order_total": order.total or None,
"delivery_cost": order.delivery_cost or None,
"discount_total": order.discount_total or None,
"items": order_items_data, "items": order_items_data,
}) })
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})") _log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})")
@@ -327,6 +331,9 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
imported_count = 0 imported_count = 0
error_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): for i, order in enumerate(truly_importable):
shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order) shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
@@ -338,7 +345,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
result = await asyncio.to_thread( result = await asyncio.to_thread(
import_service.import_single_order, import_service.import_single_order,
order, id_pol=id_pol, id_sectie=id_sectie order, id_pol=id_pol, id_sectie=id_sectie,
app_settings=app_settings
) )
# Build order items data for storage (R9) # Build order items data for storage (R9)
@@ -368,6 +376,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
payment_method=payment_method, payment_method=payment_method,
delivery_method=delivery_method, delivery_method=delivery_method,
order_total=order.total or None, order_total=order.total or None,
delivery_cost=order.delivery_cost or None,
discount_total=order.discount_total or None,
) )
await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED") await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED")
# Store ROA address IDs (R9) # Store ROA address IDs (R9)
@@ -394,6 +404,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
payment_method=payment_method, payment_method=payment_method,
delivery_method=delivery_method, delivery_method=delivery_method,
order_total=order.total or None, order_total=order.total or None,
delivery_cost=order.delivery_cost or None,
discount_total=order.discount_total or None,
) )
await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR") await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR")
await sqlite_service.add_order_items(order.number, order_items_data) await sqlite_service.add_order_items(order.number, order_items_data)

View File

@@ -313,10 +313,10 @@ async function loadDashOrders() {
<td>${dateStr}</td> <td>${dateStr}</td>
${renderClientCell(o)} ${renderClientCell(o)}
<td>${o.items_count || 0}</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 class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td>
<td>${o.id_comanda || '-'}</td> <td>${o.id_comanda || '-'}</td>
<td>${invoiceBadge}</td> <td>${invoiceBadge}</td>
<td>${orderTotal}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
} }
@@ -479,6 +479,10 @@ async function openDashOrderDetail(orderNumber) {
if (detailItemsTotal) detailItemsTotal.textContent = '-'; if (detailItemsTotal) detailItemsTotal.textContent = '-';
const detailOrderTotal = document.getElementById('detailOrderTotal'); const detailOrderTotal = document.getElementById('detailOrderTotal');
if (detailOrderTotal) detailOrderTotal.textContent = '-'; 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'); const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = ''; if (mobileContainer) mobileContainer.innerHTML = '';
@@ -510,6 +514,22 @@ async function openDashOrderDetail(orderNumber) {
document.getElementById('detailError').style.display = ''; 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 = '';
}
// 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 = '';
}
const items = data.items || []; const items = data.items || [];
if (items.length === 0) { 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="8" class="text-center text-muted">Niciun articol</td></tr>';
@@ -709,3 +729,81 @@ async function saveQuickMapping() {
alert('Eroare: ' + err.message); alert('Eroare: ' + err.message);
} }
} }
// ── 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

@@ -162,8 +162,8 @@ async function loadRunOrders(runId, statusFilter, page) {
<td><code>${esc(o.order_number)}</code></td> <td><code>${esc(o.order_number)}</code></td>
<td>${esc(o.customer_name)}</td> <td>${esc(o.customer_name)}</td>
<td>${o.items_count || 0}</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-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
<td>${orderTotal}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
} }
@@ -324,6 +324,10 @@ async function openOrderDetail(orderNumber) {
if (detailItemsTotal) detailItemsTotal.textContent = '-'; if (detailItemsTotal) detailItemsTotal.textContent = '-';
const detailOrderTotal = document.getElementById('detailOrderTotal'); const detailOrderTotal = document.getElementById('detailOrderTotal');
if (detailOrderTotal) detailOrderTotal.textContent = '-'; 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'); const mobileContainer = document.getElementById('detailItemsMobile');
if (mobileContainer) mobileContainer.innerHTML = ''; if (mobileContainer) mobileContainer.innerHTML = '';
@@ -355,6 +359,22 @@ async function openOrderDetail(orderNumber) {
document.getElementById('detailError').style.display = ''; 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 = '';
}
// 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 = '';
}
const items = data.items || []; const items = data.items || [];
if (items.length === 0) { 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="8" class="text-center text-muted">Niciun articol</td></tr>';

View File

@@ -84,10 +84,10 @@
<th class="sortable" onclick="dashSortBy('order_date')">Data <span class="sort-icon" data-col="order_date"></span></th> <th class="sortable" onclick="dashSortBy('order_date')">Data <span class="sort-icon" data-col="order_date"></span></th>
<th class="sortable" onclick="dashSortBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th> <th class="sortable" onclick="dashSortBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
<th class="sortable" onclick="dashSortBy('items_count')">Art. <span class="sort-icon" data-col="items_count"></span></th> <th class="sortable" onclick="dashSortBy('items_count')">Art. <span class="sort-icon" data-col="items_count"></span></th>
<th class="text-end">Total</th>
<th class="sortable" onclick="dashSortBy('status')">Status Import <span class="sort-icon" data-col="status"></span></th> <th class="sortable" onclick="dashSortBy('status')">Status Import <span class="sort-icon" data-col="status"></span></th>
<th>ID ROA</th> <th>ID ROA</th>
<th>Factura</th> <th>Factura</th>
<th>Total</th>
</tr> </tr>
</thead> </thead>
<tbody id="dashOrdersBody"> <tbody id="dashOrdersBody">
@@ -121,8 +121,10 @@
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span> <small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
</div> </div>
</div> </div>
<div class="d-flex justify-content-end gap-3 mb-2" id="detailTotals"> <div class="d-flex justify-content-end gap-3 mb-2 flex-wrap" id="detailTotals">
<span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span> <span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span>
<span id="detailDeliveryWrap" style="display:none"><small class="text-muted">Transport:</small> <strong id="detailDeliveryCost">-</strong></span>
<span id="detailDiscountWrap" style="display:none"><small class="text-muted">Discount:</small> <strong id="detailDiscount">-</strong></span>
<span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span> <span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span>
</div> </div>
<div class="table-responsive d-none d-md-block"> <div class="table-responsive d-none d-md-block">
@@ -153,6 +155,43 @@
</div> </div>
</div> </div>
<!-- Settings Card -->
<div class="card mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Setari Import</span>
<button class="btn btn-sm btn-primary" onclick="saveAppSettings()">Salveaza</button>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label form-label-sm mb-1">CODMAT Transport</label>
<div class="position-relative">
<input type="text" class="form-control form-control-sm" id="settTransportCodmat" placeholder="ex: TRANSPORT" autocomplete="off">
<div class="autocomplete-dropdown d-none" id="settTransportAc"></div>
</div>
<small class="text-muted">Lasa gol pentru a nu adauga transport la import</small>
</div>
<div class="col-md-2">
<label class="form-label form-label-sm mb-1">TVA Transport (%)</label>
<select class="form-select form-select-sm" id="settTransportVat">
<option value="5">5%</option>
<option value="9">9%</option>
<option value="19">19%</option>
<option value="21" selected>21%</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label form-label-sm mb-1">CODMAT Discount</label>
<div class="position-relative">
<input type="text" class="form-control form-control-sm" id="settDiscountCodmat" placeholder="ex: DISCOUNT" autocomplete="off">
<div class="autocomplete-dropdown d-none" id="settDiscountAc"></div>
</div>
<small class="text-muted">Lasa gol pentru a nu adauga discount la import</small>
</div>
</div>
</div>
</div>
<!-- Quick Map Modal (used from order detail) --> <!-- Quick Map Modal (used from order detail) -->
<div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static"> <div class="modal fade" id="quickMapModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog"> <div class="modal-dialog">
@@ -183,5 +222,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/dashboard.js?v=6"></script> <script src="/static/js/dashboard.js?v=7"></script>
{% endblock %} {% endblock %}

View File

@@ -72,8 +72,8 @@
<th class="sortable" onclick="sortOrdersBy('order_number')">Nr. comanda <span class="sort-icon" data-col="order_number"></span></th> <th class="sortable" onclick="sortOrdersBy('order_number')">Nr. comanda <span class="sort-icon" data-col="order_number"></span></th>
<th class="sortable" onclick="sortOrdersBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th> <th class="sortable" onclick="sortOrdersBy('customer_name')">Client <span class="sort-icon" data-col="customer_name"></span></th>
<th class="sortable" onclick="sortOrdersBy('items_count')">Articole <span class="sort-icon" data-col="items_count"></span></th> <th class="sortable" onclick="sortOrdersBy('items_count')">Articole <span class="sort-icon" data-col="items_count"></span></th>
<th class="text-end">Total</th>
<th class="sortable" onclick="sortOrdersBy('status')">Status <span class="sort-icon" data-col="status"></span></th> <th class="sortable" onclick="sortOrdersBy('status')">Status <span class="sort-icon" data-col="status"></span></th>
<th>Total</th>
</tr> </tr>
</thead> </thead>
<tbody id="runOrdersBody"> <tbody id="runOrdersBody">
@@ -116,8 +116,10 @@
<small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span> <small class="text-muted">ID Adr. Livrare:</small> <span id="detailIdAdresaLivr">-</span>
</div> </div>
</div> </div>
<div class="d-flex justify-content-end gap-3 mb-2" id="detailTotals"> <div class="d-flex justify-content-end gap-3 mb-2 flex-wrap" id="detailTotals">
<span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span> <span><small class="text-muted">Valoare articole:</small> <strong id="detailItemsTotal">-</strong></span>
<span id="detailDeliveryWrap" style="display:none"><small class="text-muted">Transport:</small> <strong id="detailDeliveryCost">-</strong></span>
<span id="detailDiscountWrap" style="display:none"><small class="text-muted">Discount:</small> <strong id="detailDiscount">-</strong></span>
<span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span> <span><small class="text-muted">Total comanda:</small> <strong id="detailOrderTotal">-</strong></span>
</div> </div>
<div class="table-responsive d-none d-md-block"> <div class="table-responsive d-none d-md-block">
@@ -181,5 +183,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/logs.js?v=6"></script> <script src="/static/js/logs.js?v=7"></script>
{% endblock %} {% endblock %}