feat: add unified admin Marketplace Letzshop page

- Add new Marketplace section in admin sidebar with Letzshop sub-item
- Remove old Import and Letzshop Orders items from Product Catalog
- Create unified Letzshop management page with 3 tabs:
  - Products tab: Import/Export functionality
  - Orders tab: Order management with confirm/reject/tracking
  - Settings tab: API credentials and CSV URLs
- Add unified jobs table showing imports, exports, and order syncs
- Implement vendor autocomplete using Tom Select library (CDN + fallback)
- Add /vendors/{vendor_id}/jobs API endpoint for unified job listing
- Move database queries to service layer (LetzshopOrderService)
- Add LetzshopJobItem and LetzshopJobsListResponse schemas
- Include Tom Select CSS/JS assets as local fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 18:27:04 +01:00
parent bacd79eeac
commit 45b09d6d90
14 changed files with 2589 additions and 8 deletions

View File

@@ -19,6 +19,7 @@ from models.database.letzshop import (
LetzshopSyncLog,
VendorLetzshopCredentials,
)
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)
@@ -316,3 +317,101 @@ class LetzshopOrderService:
.all()
)
return items, total
# =========================================================================
# Unified Jobs Operations
# =========================================================================
def list_letzshop_jobs(
self,
vendor_id: int,
job_type: str | None = None,
status: str | None = None,
skip: int = 0,
limit: int = 20,
) -> tuple[list[dict[str, Any]], int]:
"""
List unified Letzshop-related jobs for a vendor.
Combines product imports from marketplace_import_jobs and
order syncs from letzshop_sync_logs.
Args:
vendor_id: Vendor ID
job_type: Filter by type ('import', 'order_sync', or None for all)
status: Filter by status
skip: Pagination offset
limit: Pagination limit
Returns:
Tuple of (jobs_list, total_count) where jobs_list contains dicts
with id, type, status, created_at, started_at, completed_at,
records_processed, records_succeeded, records_failed.
"""
jobs = []
# Product imports from marketplace_import_jobs
if job_type in (None, "import"):
import_query = self.db.query(MarketplaceImportJob).filter(
MarketplaceImportJob.vendor_id == vendor_id,
MarketplaceImportJob.marketplace == "Letzshop",
)
if status:
import_query = import_query.filter(
MarketplaceImportJob.status == status
)
import_jobs = import_query.order_by(
MarketplaceImportJob.created_at.desc()
).all()
for job in import_jobs:
jobs.append(
{
"id": job.id,
"type": "import",
"status": job.status,
"created_at": job.created_at,
"started_at": job.started_at,
"completed_at": job.completed_at,
"records_processed": job.total_processed or 0,
"records_succeeded": (job.imported_count or 0)
+ (job.updated_count or 0),
"records_failed": job.error_count or 0,
}
)
# Order syncs from letzshop_sync_logs
if job_type in (None, "order_sync"):
sync_query = self.db.query(LetzshopSyncLog).filter(
LetzshopSyncLog.vendor_id == vendor_id,
LetzshopSyncLog.operation_type == "order_import",
)
if status:
sync_query = sync_query.filter(LetzshopSyncLog.status == status)
sync_logs = sync_query.order_by(LetzshopSyncLog.created_at.desc()).all()
for log in sync_logs:
jobs.append(
{
"id": log.id,
"type": "order_sync",
"status": log.status,
"created_at": log.created_at,
"started_at": log.started_at,
"completed_at": log.completed_at,
"records_processed": log.records_processed or 0,
"records_succeeded": log.records_succeeded or 0,
"records_failed": log.records_failed or 0,
}
)
# Sort all jobs by created_at descending
jobs.sort(key=lambda x: x["created_at"], reverse=True)
# Get total count and apply pagination
total = len(jobs)
jobs = jobs[skip : skip + limit]
return jobs, total