feat: show Jobs tab for all vendors when no filter selected

- Add /admin/letzshop/jobs API endpoint for all jobs across vendors
- Update list_letzshop_jobs service to support optional vendor_id
- Remove x-if condition from Jobs tab button and panel
- Update JS to use global or vendor-specific endpoint based on selection
- Update jobs table subtitle to show context

🤖 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-25 00:30:33 +01:00
parent 50fd1d01c2
commit bedc979b12
5 changed files with 91 additions and 46 deletions

View File

@@ -702,6 +702,37 @@ def trigger_vendor_sync(
# ============================================================================
@router.get(
"/jobs",
response_model=LetzshopJobsListResponse,
)
def list_all_letzshop_jobs(
job_type: str | None = Query(None, description="Filter: import, export, order_sync, historical_import"),
status: str | None = Query(None, description="Filter by status"),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get unified list of all Letzshop-related jobs across all vendors.
Combines product imports, exports, and order syncs.
"""
order_service = get_order_service(db)
jobs_data, total = order_service.list_letzshop_jobs(
vendor_id=None,
job_type=job_type,
status=status,
skip=skip,
limit=limit,
)
jobs = [LetzshopJobItem(**job) for job in jobs_data]
return LetzshopJobsListResponse(jobs=jobs, total=total)
@router.get(
"/vendors/{vendor_id}/jobs",
response_model=LetzshopJobsListResponse,

View File

@@ -607,29 +607,37 @@ class LetzshopOrderService:
def list_letzshop_jobs(
self,
vendor_id: int,
vendor_id: int | None = None,
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.
List unified Letzshop-related jobs for a vendor or all vendors.
Combines product imports, historical order imports, and order syncs.
If vendor_id is None, returns jobs across all vendors.
"""
jobs = []
# Fetch vendor info once for all jobs
vendor = self.get_vendor(vendor_id)
vendor_name = vendor.name if vendor else None
vendor_code = vendor.vendor_code if vendor else None
# Fetch vendor info - for single vendor or build lookup for all vendors
if vendor_id:
vendor = self.get_vendor(vendor_id)
vendor_lookup = {vendor_id: (vendor.name if vendor else None, vendor.vendor_code if vendor else None)}
else:
# Build lookup for all vendors when showing all jobs
from models.database.vendor import Vendor
vendors = self.db.query(Vendor.id, Vendor.name, Vendor.vendor_code).all()
vendor_lookup = {v.id: (v.name, v.vendor_code) for v in vendors}
# Historical order imports from letzshop_historical_import_jobs
if job_type in (None, "historical_import"):
hist_query = self.db.query(LetzshopHistoricalImportJob).filter(
LetzshopHistoricalImportJob.vendor_id == vendor_id,
)
hist_query = self.db.query(LetzshopHistoricalImportJob)
if vendor_id:
hist_query = hist_query.filter(
LetzshopHistoricalImportJob.vendor_id == vendor_id,
)
if status:
hist_query = hist_query.filter(
LetzshopHistoricalImportJob.status == status
@@ -640,6 +648,7 @@ class LetzshopOrderService:
).all()
for job in hist_jobs:
v_name, v_code = vendor_lookup.get(job.vendor_id, (None, None))
jobs.append(
{
"id": job.id,
@@ -652,9 +661,9 @@ class LetzshopOrderService:
"records_succeeded": (job.orders_imported or 0)
+ (job.orders_updated or 0),
"records_failed": job.orders_skipped or 0,
"vendor_id": vendor_id,
"vendor_name": vendor_name,
"vendor_code": vendor_code,
"vendor_id": job.vendor_id,
"vendor_name": v_name,
"vendor_code": v_code,
"current_phase": job.current_phase,
"error_message": job.error_message,
}
@@ -663,9 +672,12 @@ class LetzshopOrderService:
# 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 vendor_id:
import_query = import_query.filter(
MarketplaceImportJob.vendor_id == vendor_id,
)
if status:
import_query = import_query.filter(
MarketplaceImportJob.status == status
@@ -676,6 +688,7 @@ class LetzshopOrderService:
).all()
for job in import_jobs:
v_name, v_code = vendor_lookup.get(job.vendor_id, (None, None))
jobs.append(
{
"id": job.id,
@@ -688,24 +701,26 @@ class LetzshopOrderService:
"records_succeeded": (job.imported_count or 0)
+ (job.updated_count or 0),
"records_failed": job.error_count or 0,
"vendor_id": vendor_id,
"vendor_name": vendor_name,
"vendor_code": vendor_code,
"vendor_id": job.vendor_id,
"vendor_name": v_name,
"vendor_code": v_code,
}
)
# 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 vendor_id:
sync_query = sync_query.filter(LetzshopSyncLog.vendor_id == vendor_id)
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:
v_name, v_code = vendor_lookup.get(log.vendor_id, (None, None))
jobs.append(
{
"id": log.id,
@@ -717,9 +732,9 @@ class LetzshopOrderService:
"records_processed": log.records_processed or 0,
"records_succeeded": log.records_succeeded or 0,
"records_failed": log.records_failed or 0,
"vendor_id": vendor_id,
"vendor_name": vendor_name,
"vendor_code": vendor_code,
"vendor_id": log.vendor_id,
"vendor_name": v_name,
"vendor_code": v_code,
"error_details": log.error_details,
}
)
@@ -727,9 +742,10 @@ class LetzshopOrderService:
# Product exports from letzshop_sync_logs
if job_type in (None, "export"):
export_query = self.db.query(LetzshopSyncLog).filter(
LetzshopSyncLog.vendor_id == vendor_id,
LetzshopSyncLog.operation_type == "product_export",
)
if vendor_id:
export_query = export_query.filter(LetzshopSyncLog.vendor_id == vendor_id)
if status:
export_query = export_query.filter(LetzshopSyncLog.status == status)
@@ -738,6 +754,7 @@ class LetzshopOrderService:
).all()
for log in export_logs:
v_name, v_code = vendor_lookup.get(log.vendor_id, (None, None))
jobs.append(
{
"id": log.id,
@@ -749,10 +766,10 @@ class LetzshopOrderService:
"records_processed": log.records_processed or 0,
"records_succeeded": log.records_succeeded or 0,
"records_failed": log.records_failed or 0,
"vendor_id": vendor_id,
"vendor_name": vendor_name,
"vendor_code": vendor_code,
"error_details": log.error_details, # Include export file details
"vendor_id": log.vendor_id,
"vendor_name": v_name,
"vendor_code": v_code,
"error_details": log.error_details,
}
)

View File

@@ -125,9 +125,7 @@
{{ tab_button('products', 'Products', tab_var='activeTab', icon='cube') }}
{{ tab_button('orders', 'Orders', tab_var='activeTab', icon='shopping-cart', count_var='orderStats.pending') }}
{{ tab_button('exceptions', 'Exceptions', tab_var='activeTab', icon='exclamation-circle', count_var='exceptionStats.pending') }}
<template x-if="selectedVendor">
<span>{{ tab_button('jobs', 'Jobs', tab_var='activeTab', icon='collection') }}</span>
</template>
{{ tab_button('jobs', 'Jobs', tab_var='activeTab', icon='collection') }}
<template x-if="selectedVendor">
<span>{{ tab_button('settings', 'Settings', tab_var='activeTab', icon='cog') }}</span>
</template>
@@ -155,12 +153,10 @@
{% include 'admin/partials/letzshop-exceptions-tab.html' %}
{{ endtab_panel() }}
<!-- Jobs Tab - Vendor only -->
<template x-if="selectedVendor">
{{ tab_panel('jobs', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-jobs-table.html' %}
{{ endtab_panel() }}
</template>
<!-- Jobs Tab -->
{{ tab_panel('jobs', tab_var='activeTab') }}
{% include 'admin/partials/letzshop-jobs-table.html' %}
{{ endtab_panel() }}
</div>
<!-- Tracking Modal -->

View File

@@ -6,7 +6,10 @@
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Recent Jobs</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Product imports, exports, and order sync history</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
<span x-show="selectedVendor">Product imports, exports, and order sync history</span>
<span x-show="!selectedVendor">All Letzshop jobs across all vendors</span>
</p>
</div>
<button
@click="loadJobs()"

View File

@@ -158,7 +158,8 @@ function adminMarketplaceLetzshop() {
// Watch for tab changes to reload relevant data
this.$watch('activeTab', async (newTab) => {
marketplaceLetzshopLog.info('Tab changed to:', newTab);
if (newTab === 'jobs' && this.selectedVendor) {
if (newTab === 'jobs') {
// Load jobs for selected vendor or all Letzshop jobs
await this.loadJobs();
} else if (newTab === 'products') {
// Load products for selected vendor or all Letzshop products
@@ -1418,17 +1419,9 @@ function adminMarketplaceLetzshop() {
},
/**
* Load jobs for selected vendor
* Note: Jobs are vendor-specific, so we need a vendor selected to show them
* Load jobs for selected vendor or all vendors
*/
async loadJobs() {
// Jobs require a vendor to be selected (they are vendor-specific)
if (!this.selectedVendor) {
this.jobs = [];
this.jobsPagination.total = 0;
return;
}
this.loadingJobs = true;
try {
@@ -1444,7 +1437,12 @@ function adminMarketplaceLetzshop() {
params.append('status', this.jobsFilter.status);
}
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/jobs?${params}`);
// Use vendor-specific or global endpoint based on selection
const endpoint = this.selectedVendor
? `/admin/letzshop/vendors/${this.selectedVendor.id}/jobs?${params}`
: `/admin/letzshop/jobs?${params}`;
const response = await apiClient.get(endpoint);
this.jobs = response.jobs || [];
this.jobsPagination.total = response.total || 0;
} catch (error) {