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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user