# Letzshop Jobs & Tables Improvements

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

## Status: Completed

### Completed
- [x] Phase 1: Job Details Modal (commit cef80af)
- [x] Phase 2: Add vendor column to jobs table
- [x] Phase 3: Platform settings system (rows per page)
- [x] Phase 4: Numbered pagination for jobs table
- [x] 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:

```html
<!-- 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:
```javascript
showJobDetailsModal: false,
selectedJobDetails: null,
```

Update `viewJobDetails` method:
```javascript
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

### Recommended: Option A (Current Behavior)
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:
```python
# 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:
```html
<th class="px-4 py-3">Vendor</th>
```

Add column data:
```html
<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:
```python
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:
```html
{% 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

```javascript
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)

```python
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`

```python
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`

```python
@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

```javascript
// 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

```javascript
// 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`

```python
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`

```python
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`

```python
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`

```python
@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`

```javascript
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:
```html
{{ 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*
