From b52313faf66a4f6d2b2a33b80059f37f9ca5c750 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 27 Mar 2026 12:38:10 +0000 Subject: [PATCH] feat(flow): map SKU + auto-retry consolidated banner After saving a SKU mapping, check for SKIPPED orders containing that SKU and show a floating banner with count + "Importa" button. Batch retries up to 20 orders and shows result feedback. Backend: - get_skipped_orders_with_sku() in sqlite_service.py - GET /api/orders/by-sku/{sku}/pending endpoint - POST /api/orders/batch-retry endpoint (max 20, sequential) Frontend: - Auto-retry banner after quickMap save with batch import button - Success/error feedback, auto-dismiss after 15s Cache-bust: shared.js?v=19 Co-Authored-By: Claude Opus 4.6 (1M context) --- api/app/routers/sync.py | 36 ++++++++++++++++++++++++++++++ api/app/services/sqlite_service.py | 18 +++++++++++++++ api/app/static/js/shared.js | 34 ++++++++++++++++++++++++++++ api/app/templates/base.html | 4 ++-- 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index 3a7f6b4..1cdb067 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -494,6 +494,42 @@ async def retry_order(order_number: str): return result +@router.get("/api/orders/by-sku/{sku}/pending") +async def get_pending_orders_for_sku(sku: str): + """Get SKIPPED orders that contain the given SKU.""" + order_numbers = await sqlite_service.get_skipped_orders_with_sku(sku) + return {"sku": sku, "order_numbers": order_numbers, "count": len(order_numbers)} + + +@router.post("/api/orders/batch-retry") +async def batch_retry_orders(request: Request): + """Batch retry multiple orders.""" + from ..services import retry_service + body = await request.json() + order_numbers = body.get("order_numbers", []) + if not order_numbers: + return {"success": False, "message": "No orders specified"} + + app_settings = await sqlite_service.get_app_settings() + results = {"imported": 0, "errors": 0, "messages": []} + + for on in order_numbers[:20]: # Limit to 20 to avoid timeout + result = await retry_service.retry_single_order(str(on), app_settings) + if result.get("success"): + results["imported"] += 1 + else: + results["errors"] += 1 + results["messages"].append(f"{on}: {result.get('message', 'Error')}") + + return { + "success": results["imported"] > 0, + "imported": results["imported"], + "errors": results["errors"], + "message": f"{results['imported']} importate, {results['errors']} erori" if results["errors"] else f"{results['imported']} importate cu succes", + "details": results["messages"][:5], + } + + @router.get("/api/dashboard/orders") async def dashboard_orders(page: int = 1, per_page: int = 50, search: str = "", status: str = "all", diff --git a/api/app/services/sqlite_service.py b/api/app/services/sqlite_service.py index f9d8a2c..9dfc476 100644 --- a/api/app/services/sqlite_service.py +++ b/api/app/services/sqlite_service.py @@ -960,6 +960,24 @@ async def set_app_setting(key: str, value: str): await db.close() +# ── SKU-based order lookup ──────────────────────── + +async def get_skipped_orders_with_sku(sku: str) -> list[str]: + """Get order_numbers of SKIPPED orders that contain the given SKU.""" + db = await get_sqlite() + try: + cursor = await db.execute(""" + SELECT DISTINCT oi.order_number + FROM order_items oi + JOIN orders o ON o.order_number = oi.order_number + WHERE oi.sku = ? AND o.status = 'SKIPPED' + """, (sku,)) + rows = await cursor.fetchall() + return [row[0] for row in rows] + finally: + await db.close() + + # ── Price Sync Runs ─────────────────────────────── async def get_price_sync_runs(page: int = 1, per_page: int = 20): diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js index 6ee13e1..34a18fd 100644 --- a/api/app/static/js/shared.js +++ b/api/app/static/js/shared.js @@ -344,6 +344,40 @@ async function saveQuickMapping() { if (data.success) { bootstrap.Modal.getInstance(document.getElementById('quickMapModal')).hide(); if (_qmOnSave) _qmOnSave(sku, mappings); + // Check for SKIPPED orders that can now be imported + try { + const pendingRes = await fetch(`/api/orders/by-sku/${encodeURIComponent(sku)}/pending`); + const pendingData = await pendingRes.json(); + if (pendingData.count > 0) { + const banner = document.createElement('div'); + banner.className = 'alert alert-info d-flex align-items-center gap-2 mt-2'; + banner.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1060;min-width:300px;max-width:500px;box-shadow:var(--card-shadow)'; + banner.innerHTML = ` ${pendingData.count} comenzi SKIPPED pot fi importate acum `; + document.body.appendChild(banner); + + document.getElementById('batchRetryBtn').onclick = async function() { + this.disabled = true; + this.innerHTML = ''; + try { + const retryRes = await fetch('/api/orders/batch-retry', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({order_numbers: pendingData.order_numbers}) + }); + const retryData = await retryRes.json(); + banner.className = retryData.errors > 0 ? 'alert alert-warning d-flex align-items-center gap-2 mt-2' : 'alert alert-success d-flex align-items-center gap-2 mt-2'; + banner.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1060;min-width:300px;max-width:500px;box-shadow:var(--card-shadow)'; + banner.innerHTML = ` ${esc(retryData.message)} `; + setTimeout(() => banner.remove(), 5000); + if (typeof loadDashOrders === 'function') loadDashOrders(); + } catch(e) { + banner.innerHTML = `Eroare: ${esc(e.message)} `; + } + }; + + setTimeout(() => { if (banner.parentElement) banner.remove(); }, 15000); + } + } catch(e) { /* ignore */ } } else { alert('Eroare: ' + (data.error || 'Unknown')); } diff --git a/api/app/templates/base.html b/api/app/templates/base.html index dc94174..37dd61c 100644 --- a/api/app/templates/base.html +++ b/api/app/templates/base.html @@ -19,7 +19,7 @@ {% set rp = request.scope.get('root_path', '') %} - + @@ -144,7 +144,7 @@ - +