Files
orion/app/modules/marketplace/docs/job-queue.md
Samir Boulahtit f141cc4e6a docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's
docs/ folder, accessible via symlinks from docs/modules/. Create
data-model.md files for 10 modules with full schema documentation.
Replace originals with redirect stubs. Remove empty guide stubs.

Modules migrated: tenancy, billing, loyalty, marketplace, orders,
messaging, cms, catalog, inventory, hosting, prospecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:38:37 +01:00

20 KiB
Raw Permalink 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 store 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 store 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 store 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">Store:</span> <span x-text="selectedJobDetails?.store_name || selectedStore?.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 store 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="selectedStore">
  • This is intentional for store-specific features

Decision Required

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

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

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

  • All tabs visible
  • Content shows prompt to select store

The current behavior is correct because:

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

No change needed - document this as intentional behavior.


3. Add Store Column to Jobs Table

Requirements

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

Implementation

3.1 Update API Response

File: app/services/letzshop/order_service.py

Add store info to job dicts:

# In list_letzshop_jobs, add to each job dict:
"store_id": store_id,
"store_name": store.name if store else None,
"store_code": store.store_code if store else None,

Need to fetch store 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">Store</th>

Add column data:

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

3.3 Update Schema

File: models/schema/letzshop.py

Update LetzshopJobItem to include store fields:

store_id: int | None = None
store_name: str | None = None
store_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: Store 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 stores
  • Search and filter capabilities
  • View customer details and order history
  • Link to store context

Implementation

6.1 Database Model Check

File: models/database/customer.py

Verify Customer model exists with fields:

  • id, store_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,
        store_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, store_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),
    store_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
    store_id: int
    store_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_store: 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 (store 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: '',
        storeFilter: '',
        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
Store Filter Filter by store
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: Store 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
Store Column Small
Platform Settings Medium
Table Harmonization Large
Admin Customer Page Medium

Total: Large effort


Plan created: 2024-12-20