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(
|
@router.get(
|
||||||
"/vendors/{vendor_id}/jobs",
|
"/vendors/{vendor_id}/jobs",
|
||||||
response_model=LetzshopJobsListResponse,
|
response_model=LetzshopJobsListResponse,
|
||||||
|
|||||||
@@ -607,29 +607,37 @@ class LetzshopOrderService:
|
|||||||
|
|
||||||
def list_letzshop_jobs(
|
def list_letzshop_jobs(
|
||||||
self,
|
self,
|
||||||
vendor_id: int,
|
vendor_id: int | None = None,
|
||||||
job_type: str | None = None,
|
job_type: str | None = None,
|
||||||
status: str | None = None,
|
status: str | None = None,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
) -> tuple[list[dict[str, Any]], int]:
|
) -> 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.
|
Combines product imports, historical order imports, and order syncs.
|
||||||
|
If vendor_id is None, returns jobs across all vendors.
|
||||||
"""
|
"""
|
||||||
jobs = []
|
jobs = []
|
||||||
|
|
||||||
# Fetch vendor info once for all jobs
|
# Fetch vendor info - for single vendor or build lookup for all vendors
|
||||||
vendor = self.get_vendor(vendor_id)
|
if vendor_id:
|
||||||
vendor_name = vendor.name if vendor else None
|
vendor = self.get_vendor(vendor_id)
|
||||||
vendor_code = vendor.vendor_code if vendor else None
|
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
|
# Historical order imports from letzshop_historical_import_jobs
|
||||||
if job_type in (None, "historical_import"):
|
if job_type in (None, "historical_import"):
|
||||||
hist_query = self.db.query(LetzshopHistoricalImportJob).filter(
|
hist_query = self.db.query(LetzshopHistoricalImportJob)
|
||||||
LetzshopHistoricalImportJob.vendor_id == vendor_id,
|
if vendor_id:
|
||||||
)
|
hist_query = hist_query.filter(
|
||||||
|
LetzshopHistoricalImportJob.vendor_id == vendor_id,
|
||||||
|
)
|
||||||
if status:
|
if status:
|
||||||
hist_query = hist_query.filter(
|
hist_query = hist_query.filter(
|
||||||
LetzshopHistoricalImportJob.status == status
|
LetzshopHistoricalImportJob.status == status
|
||||||
@@ -640,6 +648,7 @@ class LetzshopOrderService:
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
for job in hist_jobs:
|
for job in hist_jobs:
|
||||||
|
v_name, v_code = vendor_lookup.get(job.vendor_id, (None, None))
|
||||||
jobs.append(
|
jobs.append(
|
||||||
{
|
{
|
||||||
"id": job.id,
|
"id": job.id,
|
||||||
@@ -652,9 +661,9 @@ class LetzshopOrderService:
|
|||||||
"records_succeeded": (job.orders_imported or 0)
|
"records_succeeded": (job.orders_imported or 0)
|
||||||
+ (job.orders_updated or 0),
|
+ (job.orders_updated or 0),
|
||||||
"records_failed": job.orders_skipped or 0,
|
"records_failed": job.orders_skipped or 0,
|
||||||
"vendor_id": vendor_id,
|
"vendor_id": job.vendor_id,
|
||||||
"vendor_name": vendor_name,
|
"vendor_name": v_name,
|
||||||
"vendor_code": vendor_code,
|
"vendor_code": v_code,
|
||||||
"current_phase": job.current_phase,
|
"current_phase": job.current_phase,
|
||||||
"error_message": job.error_message,
|
"error_message": job.error_message,
|
||||||
}
|
}
|
||||||
@@ -663,9 +672,12 @@ class LetzshopOrderService:
|
|||||||
# Product imports from marketplace_import_jobs
|
# Product imports from marketplace_import_jobs
|
||||||
if job_type in (None, "import"):
|
if job_type in (None, "import"):
|
||||||
import_query = self.db.query(MarketplaceImportJob).filter(
|
import_query = self.db.query(MarketplaceImportJob).filter(
|
||||||
MarketplaceImportJob.vendor_id == vendor_id,
|
|
||||||
MarketplaceImportJob.marketplace == "Letzshop",
|
MarketplaceImportJob.marketplace == "Letzshop",
|
||||||
)
|
)
|
||||||
|
if vendor_id:
|
||||||
|
import_query = import_query.filter(
|
||||||
|
MarketplaceImportJob.vendor_id == vendor_id,
|
||||||
|
)
|
||||||
if status:
|
if status:
|
||||||
import_query = import_query.filter(
|
import_query = import_query.filter(
|
||||||
MarketplaceImportJob.status == status
|
MarketplaceImportJob.status == status
|
||||||
@@ -676,6 +688,7 @@ class LetzshopOrderService:
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
for job in import_jobs:
|
for job in import_jobs:
|
||||||
|
v_name, v_code = vendor_lookup.get(job.vendor_id, (None, None))
|
||||||
jobs.append(
|
jobs.append(
|
||||||
{
|
{
|
||||||
"id": job.id,
|
"id": job.id,
|
||||||
@@ -688,24 +701,26 @@ class LetzshopOrderService:
|
|||||||
"records_succeeded": (job.imported_count or 0)
|
"records_succeeded": (job.imported_count or 0)
|
||||||
+ (job.updated_count or 0),
|
+ (job.updated_count or 0),
|
||||||
"records_failed": job.error_count or 0,
|
"records_failed": job.error_count or 0,
|
||||||
"vendor_id": vendor_id,
|
"vendor_id": job.vendor_id,
|
||||||
"vendor_name": vendor_name,
|
"vendor_name": v_name,
|
||||||
"vendor_code": vendor_code,
|
"vendor_code": v_code,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Order syncs from letzshop_sync_logs
|
# Order syncs from letzshop_sync_logs
|
||||||
if job_type in (None, "order_sync"):
|
if job_type in (None, "order_sync"):
|
||||||
sync_query = self.db.query(LetzshopSyncLog).filter(
|
sync_query = self.db.query(LetzshopSyncLog).filter(
|
||||||
LetzshopSyncLog.vendor_id == vendor_id,
|
|
||||||
LetzshopSyncLog.operation_type == "order_import",
|
LetzshopSyncLog.operation_type == "order_import",
|
||||||
)
|
)
|
||||||
|
if vendor_id:
|
||||||
|
sync_query = sync_query.filter(LetzshopSyncLog.vendor_id == vendor_id)
|
||||||
if status:
|
if status:
|
||||||
sync_query = sync_query.filter(LetzshopSyncLog.status == status)
|
sync_query = sync_query.filter(LetzshopSyncLog.status == status)
|
||||||
|
|
||||||
sync_logs = sync_query.order_by(LetzshopSyncLog.created_at.desc()).all()
|
sync_logs = sync_query.order_by(LetzshopSyncLog.created_at.desc()).all()
|
||||||
|
|
||||||
for log in sync_logs:
|
for log in sync_logs:
|
||||||
|
v_name, v_code = vendor_lookup.get(log.vendor_id, (None, None))
|
||||||
jobs.append(
|
jobs.append(
|
||||||
{
|
{
|
||||||
"id": log.id,
|
"id": log.id,
|
||||||
@@ -717,9 +732,9 @@ class LetzshopOrderService:
|
|||||||
"records_processed": log.records_processed or 0,
|
"records_processed": log.records_processed or 0,
|
||||||
"records_succeeded": log.records_succeeded or 0,
|
"records_succeeded": log.records_succeeded or 0,
|
||||||
"records_failed": log.records_failed or 0,
|
"records_failed": log.records_failed or 0,
|
||||||
"vendor_id": vendor_id,
|
"vendor_id": log.vendor_id,
|
||||||
"vendor_name": vendor_name,
|
"vendor_name": v_name,
|
||||||
"vendor_code": vendor_code,
|
"vendor_code": v_code,
|
||||||
"error_details": log.error_details,
|
"error_details": log.error_details,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -727,9 +742,10 @@ class LetzshopOrderService:
|
|||||||
# Product exports from letzshop_sync_logs
|
# Product exports from letzshop_sync_logs
|
||||||
if job_type in (None, "export"):
|
if job_type in (None, "export"):
|
||||||
export_query = self.db.query(LetzshopSyncLog).filter(
|
export_query = self.db.query(LetzshopSyncLog).filter(
|
||||||
LetzshopSyncLog.vendor_id == vendor_id,
|
|
||||||
LetzshopSyncLog.operation_type == "product_export",
|
LetzshopSyncLog.operation_type == "product_export",
|
||||||
)
|
)
|
||||||
|
if vendor_id:
|
||||||
|
export_query = export_query.filter(LetzshopSyncLog.vendor_id == vendor_id)
|
||||||
if status:
|
if status:
|
||||||
export_query = export_query.filter(LetzshopSyncLog.status == status)
|
export_query = export_query.filter(LetzshopSyncLog.status == status)
|
||||||
|
|
||||||
@@ -738,6 +754,7 @@ class LetzshopOrderService:
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
for log in export_logs:
|
for log in export_logs:
|
||||||
|
v_name, v_code = vendor_lookup.get(log.vendor_id, (None, None))
|
||||||
jobs.append(
|
jobs.append(
|
||||||
{
|
{
|
||||||
"id": log.id,
|
"id": log.id,
|
||||||
@@ -749,10 +766,10 @@ class LetzshopOrderService:
|
|||||||
"records_processed": log.records_processed or 0,
|
"records_processed": log.records_processed or 0,
|
||||||
"records_succeeded": log.records_succeeded or 0,
|
"records_succeeded": log.records_succeeded or 0,
|
||||||
"records_failed": log.records_failed or 0,
|
"records_failed": log.records_failed or 0,
|
||||||
"vendor_id": vendor_id,
|
"vendor_id": log.vendor_id,
|
||||||
"vendor_name": vendor_name,
|
"vendor_name": v_name,
|
||||||
"vendor_code": vendor_code,
|
"vendor_code": v_code,
|
||||||
"error_details": log.error_details, # Include export file details
|
"error_details": log.error_details,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -125,9 +125,7 @@
|
|||||||
{{ tab_button('products', 'Products', tab_var='activeTab', icon='cube') }}
|
{{ 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('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') }}
|
{{ tab_button('exceptions', 'Exceptions', tab_var='activeTab', icon='exclamation-circle', count_var='exceptionStats.pending') }}
|
||||||
<template x-if="selectedVendor">
|
{{ tab_button('jobs', 'Jobs', tab_var='activeTab', icon='collection') }}
|
||||||
<span>{{ tab_button('jobs', 'Jobs', tab_var='activeTab', icon='collection') }}</span>
|
|
||||||
</template>
|
|
||||||
<template x-if="selectedVendor">
|
<template x-if="selectedVendor">
|
||||||
<span>{{ tab_button('settings', 'Settings', tab_var='activeTab', icon='cog') }}</span>
|
<span>{{ tab_button('settings', 'Settings', tab_var='activeTab', icon='cog') }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -155,12 +153,10 @@
|
|||||||
{% include 'admin/partials/letzshop-exceptions-tab.html' %}
|
{% include 'admin/partials/letzshop-exceptions-tab.html' %}
|
||||||
{{ endtab_panel() }}
|
{{ endtab_panel() }}
|
||||||
|
|
||||||
<!-- Jobs Tab - Vendor only -->
|
<!-- Jobs Tab -->
|
||||||
<template x-if="selectedVendor">
|
{{ tab_panel('jobs', tab_var='activeTab') }}
|
||||||
{{ tab_panel('jobs', tab_var='activeTab') }}
|
{% include 'admin/partials/letzshop-jobs-table.html' %}
|
||||||
{% include 'admin/partials/letzshop-jobs-table.html' %}
|
{{ endtab_panel() }}
|
||||||
{{ endtab_panel() }}
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tracking Modal -->
|
<!-- Tracking Modal -->
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Recent Jobs</h3>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="loadJobs()"
|
@click="loadJobs()"
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ function adminMarketplaceLetzshop() {
|
|||||||
// Watch for tab changes to reload relevant data
|
// Watch for tab changes to reload relevant data
|
||||||
this.$watch('activeTab', async (newTab) => {
|
this.$watch('activeTab', async (newTab) => {
|
||||||
marketplaceLetzshopLog.info('Tab changed to:', 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();
|
await this.loadJobs();
|
||||||
} else if (newTab === 'products') {
|
} else if (newTab === 'products') {
|
||||||
// Load products for selected vendor or all Letzshop products
|
// Load products for selected vendor or all Letzshop products
|
||||||
@@ -1418,17 +1419,9 @@ function adminMarketplaceLetzshop() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load jobs for selected vendor
|
* Load jobs for selected vendor or all vendors
|
||||||
* Note: Jobs are vendor-specific, so we need a vendor selected to show them
|
|
||||||
*/
|
*/
|
||||||
async loadJobs() {
|
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;
|
this.loadingJobs = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1444,7 +1437,12 @@ function adminMarketplaceLetzshop() {
|
|||||||
params.append('status', this.jobsFilter.status);
|
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.jobs = response.jobs || [];
|
||||||
this.jobsPagination.total = response.total || 0;
|
this.jobsPagination.total = response.total || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user