Files
orion/docs/implementation/letzshop-jobs-improvements.md
Samir Boulahtit a118edced5 feat: enhance Letzshop jobs and order management
- Add job cancellation and retry functionality
- Improve jobs table with better status display
- Add background task improvements
- Update Letzshop order service
- Update documentation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 14:12:26 +01:00

20 KiB
Raw Blame History

Letzshop Jobs & Tables Improvements

Implementation plan for improving the Letzshop management page jobs display and table harmonization.

Status: Completed

Completed

  • Phase 1: Job Details Modal (commit cef80af)
  • Phase 2: Add vendor column to jobs table
  • Phase 3: Platform settings system (rows per page)
  • Phase 4: Numbered pagination for jobs table
  • Phase 5: Admin customer management page

Overview

This plan addresses 6 improvements:

  1. Job details modal with proper display
  2. Tab visibility fix when filters cleared
  3. Add vendor column to jobs table
  4. Harmonize all tables with table macro
  5. Platform-wide rows per page setting
  6. Build admin customer page

1. Job Details Modal

Current Issue

  • "View Details" shows a browser alert instead of a proper modal
  • No detailed breakdown of export results

Requirements

  • Create a proper modal for job details
  • For exports: show products exported per language file
  • Show vendor name/code
  • Show full timestamps and duration
  • Show error details if any

Implementation

1.1 Create Job Details Modal Template

File: app/templates/admin/partials/letzshop-jobs-table.html

Add modal after the table:

<!-- Job Details Modal -->
<div
    x-show="showJobDetailsModal"
    x-transition
    class="fixed inset-0 z-30 flex items-center justify-center bg-black bg-opacity-50"
    @click.self="showJobDetailsModal = false"
    x-cloak
>
    <div class="w-full max-w-lg bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6">
        <header class="flex justify-between items-center mb-4">
            <h3 class="text-lg font-semibold">Job Details</h3>
            <button @click="showJobDetailsModal = false">×</button>
        </header>

        <div class="space-y-4">
            <!-- Job Info -->
            <div class="grid grid-cols-2 gap-4 text-sm">
                <div><span class="font-medium">Job ID:</span> #<span x-text="selectedJobDetails?.id"></span></div>
                <div><span class="font-medium">Type:</span> <span x-text="selectedJobDetails?.type"></span></div>
                <div><span class="font-medium">Status:</span> <span x-text="selectedJobDetails?.status"></span></div>
                <div><span class="font-medium">Vendor:</span> <span x-text="selectedJobDetails?.vendor_name || selectedVendor?.name"></span></div>
            </div>

            <!-- Timestamps -->
            <div class="text-sm">
                <p><span class="font-medium">Started:</span> <span x-text="formatDate(selectedJobDetails?.started_at)"></span></p>
                <p><span class="font-medium">Completed:</span> <span x-text="formatDate(selectedJobDetails?.completed_at)"></span></p>
                <p><span class="font-medium">Duration:</span> <span x-text="formatDuration(selectedJobDetails?.started_at, selectedJobDetails?.completed_at)"></span></p>
            </div>

            <!-- Export Details (for export jobs) -->
            <template x-if="selectedJobDetails?.type === 'export' && selectedJobDetails?.error_details?.products_exported">
                <div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
                    <h4 class="font-medium mb-2">Export Details</h4>
                    <p class="text-sm">Products exported: <span x-text="selectedJobDetails.error_details.products_exported"></span></p>
                    <template x-if="selectedJobDetails.error_details.files">
                        <div class="mt-2 space-y-1">
                            <template x-for="file in selectedJobDetails.error_details.files" :key="file.language">
                                <div class="text-xs flex justify-between">
                                    <span x-text="file.language.toUpperCase()"></span>
                                    <span x-text="file.error ? 'Failed: ' + file.error : file.filename + ' (' + (file.size_bytes / 1024).toFixed(1) + ' KB)'"></span>
                                </div>
                            </template>
                        </div>
                    </template>
                </div>
            </template>

            <!-- Error Details -->
            <template x-if="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error">
                <div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
                    <h4 class="font-medium text-red-700 mb-2">Error</h4>
                    <p class="text-sm text-red-600" x-text="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error"></p>
                </div>
            </template>
        </div>
    </div>
</div>

1.2 Update JavaScript State

File: static/admin/js/marketplace-letzshop.js

Add state variables:

showJobDetailsModal: false,
selectedJobDetails: null,

Update viewJobDetails method:

viewJobDetails(job) {
    this.selectedJobDetails = job;
    this.showJobDetailsModal = true;
},

1.3 Update API to Return Full Details

File: app/services/letzshop/order_service.py

Update list_letzshop_jobs to include error_details in the response for export jobs.


2. Tab Visibility Fix

Current Issue

  • When vendor filter is cleared, only 2 tabs appear (Orders, Exceptions)
  • Should show all tabs: Products, Orders, Exceptions, Jobs, Settings

Root Cause

  • Products, Jobs, and Settings tabs are wrapped in <template x-if="selectedVendor">
  • This is intentional for vendor-specific features

Decision Required

Option A: Keep current behavior (vendor-specific tabs hidden when no vendor)

  • Products, Jobs, Settings require a vendor context
  • Cross-vendor view only shows Orders and Exceptions

Option B: Show all tabs but with "Select vendor" message

  • All tabs visible
  • Content shows prompt to select vendor

The current behavior is correct because:

  • Products tab shows vendor's Letzshop products (needs vendor)
  • Jobs tab shows vendor's jobs (needs vendor)
  • Settings tab configures vendor's Letzshop (needs vendor)
  • Orders and Exceptions can work cross-vendor

No change needed - document this as intentional behavior.


3. Add Vendor Column to Jobs Table

Requirements

  • Add vendor name/code column to jobs table
  • Useful when viewing cross-vendor (future feature)
  • Prepare for reusable jobs component

Implementation

3.1 Update API Response

File: app/services/letzshop/order_service.py

Add vendor info to job dicts:

# In list_letzshop_jobs, add to each job dict:
"vendor_id": vendor_id,
"vendor_name": vendor.name if vendor else None,
"vendor_code": vendor.vendor_code if vendor else None,

Need to fetch vendor once at start of function.

3.2 Update Table Template

File: app/templates/admin/partials/letzshop-jobs-table.html

Add column header:

<th class="px-4 py-3">Vendor</th>

Add column data:

<td class="px-4 py-3 text-sm">
    <span x-text="job.vendor_code || job.vendor_name || '-'"></span>
</td>

3.3 Update Schema

File: models/schema/letzshop.py

Update LetzshopJobItem to include vendor fields:

vendor_id: int | None = None
vendor_name: str | None = None
vendor_code: str | None = None

4. Harmonize Tables with Table Macro

Current State

  • Different tables use different pagination styles
  • Some use simple prev/next, others use numbered
  • Inconsistent styling

Requirements

  • All tables use table macro from shared/macros/tables.html
  • Numbered pagination with page numbers
  • Consistent column styling
  • Rows per page selector

Tables to Update

Table File Current Pagination
Jobs letzshop-jobs-table.html Simple prev/next
Products letzshop-products-tab.html Simple prev/next
Orders letzshop-orders-tab.html Simple prev/next
Exceptions letzshop-exceptions-tab.html Simple prev/next

Implementation

4.1 Create/Update Table Macro

File: app/templates/shared/macros/tables.html

Ensure numbered pagination macro exists:

{% macro numbered_pagination(page_var, total_var, limit_var, on_change) %}
<div class="flex items-center justify-between mt-4">
    <div class="text-sm text-gray-600">
        Showing <span x-text="(({{ page_var }} - 1) * {{ limit_var }}) + 1"></span>
        to <span x-text="Math.min({{ page_var }} * {{ limit_var }}, {{ total_var }})"></span>
        of <span x-text="{{ total_var }}"></span>
    </div>
    <div class="flex items-center gap-1">
        <!-- First -->
        <button @click="{{ page_var }} = 1; {{ on_change }}" :disabled="{{ page_var }} <= 1">«</button>
        <!-- Prev -->
        <button @click="{{ page_var }}--; {{ on_change }}" :disabled="{{ page_var }} <= 1"></button>
        <!-- Page numbers -->
        <template x-for="p in getPageNumbers({{ page_var }}, Math.ceil({{ total_var }} / {{ limit_var }}))">
            <button @click="{{ page_var }} = p; {{ on_change }}" :class="p === {{ page_var }} ? 'bg-purple-600 text-white' : ''">
                <span x-text="p"></span>
            </button>
        </template>
        <!-- Next -->
        <button @click="{{ page_var }}++; {{ on_change }}" :disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}"></button>
        <!-- Last -->
        <button @click="{{ page_var }} = Math.ceil({{ total_var }} / {{ limit_var }}); {{ on_change }}" :disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}">»</button>
    </div>
</div>
{% endmacro %}

4.2 Add Page Numbers Helper to JavaScript

File: static/shared/js/helpers.js or inline

function getPageNumbers(current, total, maxVisible = 5) {
    if (total <= maxVisible) {
        return Array.from({length: total}, (_, i) => i + 1);
    }

    const half = Math.floor(maxVisible / 2);
    let start = Math.max(1, current - half);
    let end = Math.min(total, start + maxVisible - 1);

    if (end - start < maxVisible - 1) {
        start = Math.max(1, end - maxVisible + 1);
    }

    return Array.from({length: end - start + 1}, (_, i) => start + i);
}

4.3 Update Each Table

Update each table to use the macro and consistent styling.


5. Platform-Wide Rows Per Page Setting

Requirements

  • Global setting for default rows per page
  • Stored in platform settings (not per-user initially)
  • Used by all paginated tables
  • Options: 10, 20, 50, 100

Implementation

5.1 Add Platform Setting

File: models/database/platform_settings.py (create if doesn't exist)

class PlatformSettings(Base):
    __tablename__ = "platform_settings"

    id = Column(Integer, primary_key=True)
    key = Column(String(100), unique=True, nullable=False)
    value = Column(String(500), nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow)

Or add to existing settings table if one exists.

5.2 Create Settings Service

File: app/services/platform_settings_service.py

class PlatformSettingsService:
    def get_setting(self, db: Session, key: str, default: Any = None) -> Any:
        setting = db.query(PlatformSettings).filter_by(key=key).first()
        return setting.value if setting else default

    def set_setting(self, db: Session, key: str, value: Any) -> None:
        setting = db.query(PlatformSettings).filter_by(key=key).first()
        if setting:
            setting.value = str(value)
        else:
            setting = PlatformSettings(key=key, value=str(value))
            db.add(setting)
        db.flush()

    def get_rows_per_page(self, db: Session) -> int:
        return int(self.get_setting(db, "rows_per_page", "20"))

5.3 Expose via API

File: app/api/v1/admin/settings.py

@router.get("/platform/rows-per-page")
def get_rows_per_page(db: Session = Depends(get_db)):
    return {"rows_per_page": platform_settings_service.get_rows_per_page(db)}

@router.put("/platform/rows-per-page")
def set_rows_per_page(
    rows: int = Query(..., ge=10, le=100),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    platform_settings_service.set_setting(db, "rows_per_page", rows)
    db.commit()
    return {"rows_per_page": rows}

5.4 Load Setting in Frontend

File: static/shared/js/app.js or similar

// Load platform settings on app init
async function loadPlatformSettings() {
    try {
        const response = await apiClient.get('/admin/settings/platform/rows-per-page');
        window.platformSettings = {
            rowsPerPage: response.rows_per_page || 20
        };
    } catch {
        window.platformSettings = { rowsPerPage: 20 };
    }
}

5.5 Use in Alpine Components

// In each paginated component's init:
this.limit = window.platformSettings?.rowsPerPage || 20;

Implementation Order

  1. Phase 1: Job Details Modal (Quick win)

    • Add modal template
    • Update JS state and methods
    • Test with export jobs
  2. Phase 2: Vendor Column (Preparation)

    • Update API response
    • Update schema
    • Add column to table
  3. Phase 3: Platform Settings (Foundation)

    • Create settings model/migration
    • Create service
    • Create API endpoint
    • Frontend integration
  4. Phase 4: Table Harmonization (Largest effort)

    • Create/update table macros
    • Add pagination helper function
    • Update each table one by one
    • Test thoroughly
  5. Phase 5: Documentation

    • Update component documentation
    • Add settings documentation

Files to Create/Modify

New Files

  • models/database/platform_settings.py (if not exists)
  • app/services/platform_settings_service.py
  • alembic/versions/xxx_add_platform_settings.py

Modified Files

  • app/templates/admin/partials/letzshop-jobs-table.html
  • app/templates/admin/partials/letzshop-products-tab.html
  • app/templates/admin/partials/letzshop-orders-tab.html
  • app/templates/admin/partials/letzshop-exceptions-tab.html
  • app/templates/shared/macros/tables.html
  • static/admin/js/marketplace-letzshop.js
  • static/shared/js/helpers.js or app.js
  • app/services/letzshop/order_service.py
  • models/schema/letzshop.py
  • app/api/v1/admin/settings.py or new file

6. Admin Customer Page

Requirements

  • New page at /admin/customers to manage customers
  • List all customers across vendors
  • Search and filter capabilities
  • View customer details and order history
  • Link to vendor context

Implementation

6.1 Database Model Check

File: models/database/customer.py

Verify Customer model exists with fields:

  • id, vendor_id
  • email, name, phone
  • shipping address fields
  • created_at, updated_at

6.2 Create Customer Service

File: app/services/customer_service.py

class CustomerService:
    def get_customers(
        self,
        db: Session,
        skip: int = 0,
        limit: int = 20,
        search: str | None = None,
        vendor_id: int | None = None,
    ) -> tuple[list[dict], int]:
        """Get paginated customer list with optional filters."""
        pass

    def get_customer_detail(self, db: Session, customer_id: int) -> dict:
        """Get customer with order history."""
        pass

    def get_customer_stats(self, db: Session, vendor_id: int | None = None) -> dict:
        """Get customer statistics."""
        pass

6.3 Create API Endpoints

File: app/api/v1/admin/customers.py

router = APIRouter(prefix="/customers")

@router.get("", response_model=CustomerListResponse)
def get_customers(
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    search: str | None = Query(None),
    vendor_id: int | None = Query(None),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """List all customers with filtering."""
    pass

@router.get("/stats", response_model=CustomerStatsResponse)
def get_customer_stats(...):
    """Get customer statistics."""
    pass

@router.get("/{customer_id}", response_model=CustomerDetailResponse)
def get_customer_detail(...):
    """Get customer with order history."""
    pass

6.4 Create Pydantic Schemas

File: models/schema/customer.py

class CustomerListItem(BaseModel):
    id: int
    email: str
    name: str | None
    phone: str | None
    vendor_id: int
    vendor_name: str | None
    order_count: int
    total_spent: float
    created_at: datetime

class CustomerListResponse(BaseModel):
    customers: list[CustomerListItem]
    total: int
    skip: int
    limit: int

class CustomerDetailResponse(CustomerListItem):
    shipping_address: str | None
    orders: list[OrderSummary]

class CustomerStatsResponse(BaseModel):
    total: int
    new_this_month: int
    active: int  # ordered in last 90 days
    by_vendor: dict[str, int]

6.5 Create Admin Page Route

File: app/routes/admin_pages.py

@router.get("/customers", response_class=HTMLResponse)
async def admin_customers_page(request: Request, ...):
    return templates.TemplateResponse(
        "admin/customers.html",
        {"request": request, "current_page": "customers"}
    )

6.6 Create Template

File: app/templates/admin/customers.html

Structure:

  • Page header with title and stats
  • Search bar and filters (vendor dropdown)
  • Customer table with pagination
  • Click row to view details modal

6.7 Create Alpine Component

File: static/admin/js/customers.js

function adminCustomers() {
    return {
        customers: [],
        total: 0,
        page: 1,
        limit: 20,
        search: '',
        vendorFilter: '',
        loading: false,
        stats: {},

        async init() {
            await Promise.all([
                this.loadCustomers(),
                this.loadStats()
            ]);
        },

        async loadCustomers() { ... },
        async loadStats() { ... },
        async viewCustomer(id) { ... },
    }
}

6.8 Add to Sidebar

File: app/templates/admin/partials/sidebar.html

Add menu item:

{{ menu_item('customers', '/admin/customers', 'users', 'Customers') }}

Customer Page Features

Feature Description
List View Paginated table of all customers
Search Search by name, email, phone
Vendor Filter Filter by vendor
Stats Cards Total, new, active customers
Detail Modal Customer info + order history
Quick Actions View orders, send email

Implementation Order

  1. Phase 1: Job Details Modal (Quick win)

    • Add modal template
    • Update JS state and methods
    • Test with export jobs
  2. Phase 2: Vendor Column (Preparation)

    • Update API response
    • Update schema
    • Add column to table
  3. Phase 3: Platform Settings (Foundation)

    • Create settings model/migration
    • Create service
    • Create API endpoint
    • Frontend integration
  4. Phase 4: Table Harmonization (Largest effort)

    • Create/update table macros
    • Add pagination helper function
    • Update each table one by one
    • Test thoroughly
  5. Phase 5: Admin Customer Page

    • Create service and API
    • Create schemas
    • Create template and JS
    • Add to sidebar
  6. Phase 6: Documentation

    • Update component documentation
    • Add settings documentation
    • Add customer page documentation

Files to Create/Modify

New Files

  • models/database/platform_settings.py (if not exists)
  • app/services/platform_settings_service.py
  • app/services/customer_service.py
  • app/api/v1/admin/customers.py
  • models/schema/customer.py
  • app/templates/admin/customers.html
  • static/admin/js/customers.js
  • alembic/versions/xxx_add_platform_settings.py

Modified Files

  • app/templates/admin/partials/letzshop-jobs-table.html
  • app/templates/admin/partials/letzshop-products-tab.html
  • app/templates/admin/partials/letzshop-orders-tab.html
  • app/templates/admin/partials/letzshop-exceptions-tab.html
  • app/templates/admin/partials/sidebar.html
  • app/templates/shared/macros/tables.html
  • static/admin/js/marketplace-letzshop.js
  • static/shared/js/helpers.js or app.js
  • app/services/letzshop/order_service.py
  • models/schema/letzshop.py
  • app/api/v1/admin/__init__.py
  • app/routes/admin_pages.py

Estimated Effort

Task Effort
Job Details Modal Small
Tab Visibility (no change) None
Vendor Column Small
Platform Settings Medium
Table Harmonization Large
Admin Customer Page Medium

Total: Large effort


Plan created: 2024-12-20