Files
gomag-vending/api/app/services/sqlite_service.py
Marius Mutu 97699fa0e5 feat(dashboard): add logs page, pagination, quick mapping modal, price pre-validation
- Add /logs page with per-order sync run details, filters (Toate/Importate/Fara Mapare/Erori)
- Add price pre-validation (validate_prices + ensure_prices) to prevent ORA-20000 on direct articles
- Add find_new_orders() to detect orders not yet in Oracle COMENZI
- Extend missing_skus table with order context (order_count, order_numbers, customers)
- Add server-side pagination on /api/validate/missing-skus and /missing-skus page
- Replace confusing "Skip"/"Err" with "Fara Mapare"/"Erori" terminology
- Add inline mapping modal on dashboard (replaces navigation to /mappings)
- Add 2-row stat cards: orders (Comenzi Noi/Ready/Importate/Fara Mapare/Erori) + articles
- Add ID_POL/ID_GESTIUNE/ID_SECTIE to config.py and .env
- Update .gitignore (venv, *.db, api/api/, logs/)
- 33/33 unit tests pass, E2E verified with Playwright

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:59:08 +02:00

263 lines
8.2 KiB
Python

import json
import logging
from datetime import datetime
from ..database import get_sqlite, get_sqlite_sync
logger = logging.getLogger(__name__)
async def create_sync_run(run_id: str, json_files: int = 0):
"""Create a new sync run record."""
db = await get_sqlite()
try:
await db.execute("""
INSERT INTO sync_runs (run_id, started_at, status, json_files)
VALUES (?, datetime('now'), 'running', ?)
""", (run_id, json_files))
await db.commit()
finally:
await db.close()
async def update_sync_run(run_id: str, status: str, total_orders: int = 0,
imported: int = 0, skipped: int = 0, errors: int = 0):
"""Update sync run with results."""
db = await get_sqlite()
try:
await db.execute("""
UPDATE sync_runs SET
finished_at = datetime('now'),
status = ?,
total_orders = ?,
imported = ?,
skipped = ?,
errors = ?
WHERE run_id = ?
""", (status, total_orders, imported, skipped, errors, run_id))
await db.commit()
finally:
await db.close()
async def add_import_order(sync_run_id: str, order_number: str, order_date: str,
customer_name: str, status: str, id_comanda: int = None,
id_partener: int = None, error_message: str = None,
missing_skus: list = None, items_count: int = 0):
"""Record an individual order import result."""
db = await get_sqlite()
try:
await db.execute("""
INSERT INTO import_orders
(sync_run_id, order_number, order_date, customer_name, status,
id_comanda, id_partener, error_message, missing_skus, items_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (sync_run_id, order_number, order_date, customer_name, status,
id_comanda, id_partener, error_message,
json.dumps(missing_skus) if missing_skus else None, items_count))
await db.commit()
finally:
await db.close()
async def track_missing_sku(sku: str, product_name: str = "",
order_count: int = 0, order_numbers: str = None,
customers: str = None):
"""Track a missing SKU with order context."""
db = await get_sqlite()
try:
await db.execute("""
INSERT OR IGNORE INTO missing_skus (sku, product_name)
VALUES (?, ?)
""", (sku, product_name))
# Update context columns (always update with latest data)
if order_count or order_numbers or customers:
await db.execute("""
UPDATE missing_skus SET
order_count = ?,
order_numbers = ?,
customers = ?
WHERE sku = ?
""", (order_count, order_numbers, customers, sku))
await db.commit()
finally:
await db.close()
async def resolve_missing_sku(sku: str):
"""Mark a missing SKU as resolved."""
db = await get_sqlite()
try:
await db.execute("""
UPDATE missing_skus SET resolved = 1, resolved_at = datetime('now')
WHERE sku = ?
""", (sku,))
await db.commit()
finally:
await db.close()
async def get_missing_skus_paginated(page: int = 1, per_page: int = 20, resolved: int = 0):
"""Get paginated missing SKUs."""
db = await get_sqlite()
try:
offset = (page - 1) * per_page
cursor = await db.execute(
"SELECT COUNT(*) FROM missing_skus WHERE resolved = ?", (resolved,)
)
total = (await cursor.fetchone())[0]
cursor = await db.execute("""
SELECT sku, product_name, first_seen, resolved, resolved_at,
order_count, order_numbers, customers
FROM missing_skus
WHERE resolved = ?
ORDER BY order_count DESC, first_seen DESC
LIMIT ? OFFSET ?
""", (resolved, per_page, offset))
rows = await cursor.fetchall()
return {
"missing_skus": [dict(row) for row in rows],
"total": total,
"page": page,
"per_page": per_page,
"pages": (total + per_page - 1) // per_page if total > 0 else 0
}
finally:
await db.close()
async def get_sync_runs(page: int = 1, per_page: int = 20):
"""Get paginated sync run history."""
db = await get_sqlite()
try:
offset = (page - 1) * per_page
cursor = await db.execute("SELECT COUNT(*) FROM sync_runs")
total = (await cursor.fetchone())[0]
cursor = await db.execute("""
SELECT * FROM sync_runs
ORDER BY started_at DESC
LIMIT ? OFFSET ?
""", (per_page, offset))
rows = await cursor.fetchall()
return {
"runs": [dict(row) for row in rows],
"total": total,
"page": page,
"pages": (total + per_page - 1) // per_page if total > 0 else 0
}
finally:
await db.close()
async def get_sync_run_detail(run_id: str):
"""Get details for a specific sync run including its orders."""
db = await get_sqlite()
try:
cursor = await db.execute(
"SELECT * FROM sync_runs WHERE run_id = ?", (run_id,)
)
run = await cursor.fetchone()
if not run:
return None
cursor = await db.execute("""
SELECT * FROM import_orders
WHERE sync_run_id = ?
ORDER BY created_at
""", (run_id,))
orders = await cursor.fetchall()
return {
"run": dict(run),
"orders": [dict(o) for o in orders]
}
finally:
await db.close()
async def get_dashboard_stats():
"""Get stats for the dashboard."""
db = await get_sqlite()
try:
# Total imported
cursor = await db.execute(
"SELECT COUNT(*) FROM import_orders WHERE status = 'IMPORTED'"
)
imported = (await cursor.fetchone())[0]
# Total skipped
cursor = await db.execute(
"SELECT COUNT(*) FROM import_orders WHERE status = 'SKIPPED'"
)
skipped = (await cursor.fetchone())[0]
# Total errors
cursor = await db.execute(
"SELECT COUNT(*) FROM import_orders WHERE status = 'ERROR'"
)
errors = (await cursor.fetchone())[0]
# Missing SKUs (unresolved)
cursor = await db.execute(
"SELECT COUNT(*) FROM missing_skus WHERE resolved = 0"
)
missing = (await cursor.fetchone())[0]
# Article stats from last sync
cursor = await db.execute("""
SELECT COUNT(DISTINCT sku) FROM missing_skus
""")
total_missing_skus = (await cursor.fetchone())[0]
cursor = await db.execute("""
SELECT COUNT(DISTINCT sku) FROM missing_skus WHERE resolved = 0
""")
unresolved_skus = (await cursor.fetchone())[0]
# Last sync run
cursor = await db.execute("""
SELECT * FROM sync_runs ORDER BY started_at DESC LIMIT 1
""")
last_run = await cursor.fetchone()
return {
"imported": imported,
"skipped": skipped,
"errors": errors,
"missing_skus": missing,
"total_tracked_skus": total_missing_skus,
"unresolved_skus": unresolved_skus,
"last_run": dict(last_run) if last_run else None
}
finally:
await db.close()
async def get_scheduler_config():
"""Get scheduler configuration from SQLite."""
db = await get_sqlite()
try:
cursor = await db.execute("SELECT key, value FROM scheduler_config")
rows = await cursor.fetchall()
return {row["key"]: row["value"] for row in rows}
finally:
await db.close()
async def set_scheduler_config(key: str, value: str):
"""Set a scheduler configuration value."""
db = await get_sqlite()
try:
await db.execute("""
INSERT OR REPLACE INTO scheduler_config (key, value)
VALUES (?, ?)
""", (key, value))
await db.commit()
finally:
await db.close()