wip: update frontend templates for Letzshop order management
- Add Letzshop order detail page template - Update orders list template - Update Letzshop orders tab with improved UI - Add JavaScript for order confirmation flow Note: Frontend needs alignment with new unified order schema. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
296
app/templates/admin/letzshop-order-detail.html
Normal file
296
app/templates/admin/letzshop-order-detail.html
Normal file
@@ -0,0 +1,296 @@
|
||||
{# app/templates/admin/letzshop-order-detail.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex %}
|
||||
|
||||
{% block title %}Letzshop Order Details{% endblock %}
|
||||
{% block alpine_data %}letzshopOrderDetail(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="h-full pb-16 overflow-y-auto">
|
||||
<div class="container grid px-6 mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href="/admin/marketplace/letzshop"
|
||||
class="flex items-center text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
<span x-html="$icon('arrow-left', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Order <span x-text="order?.letzshop_order_number || 'Loading...'"></span>
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Letzshop Order Details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
x-show="order"
|
||||
class="px-3 py-1 text-sm rounded-full font-medium"
|
||||
:class="{
|
||||
'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300': order?.sync_status === 'pending',
|
||||
'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300': order?.sync_status === 'confirmed',
|
||||
'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300': order?.sync_status === 'rejected',
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300': order?.sync_status === 'shipped'
|
||||
}"
|
||||
x-text="order?.sync_status === 'rejected' ? 'DECLINED' : order?.sync_status?.toUpperCase()"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="flex justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
{{ error_state('Failed to load order', 'error') }}
|
||||
|
||||
<!-- Order Content -->
|
||||
<div x-show="order && !loading" class="grid gap-6 mb-8 md:grid-cols-2">
|
||||
<!-- Order Information -->
|
||||
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||
<span x-html="$icon('document-text', 'w-5 h-5')"></span>
|
||||
Order Information
|
||||
</h4>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Order Number</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="order?.letzshop_order_number"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Order Date</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="formatDate(order?.order_date || order?.created_at)"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Shipment ID</span>
|
||||
<span class="font-mono text-xs text-gray-600 dark:text-gray-400" x-text="order?.letzshop_shipment_id"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Total</span>
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-300" x-text="order?.total_amount + ' ' + order?.currency"></span>
|
||||
</div>
|
||||
<div class="flex justify-between" x-show="order?.confirmed_at">
|
||||
<span class="text-gray-500 dark:text-gray-400">Confirmed At</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(order?.confirmed_at)"></span>
|
||||
</div>
|
||||
<div class="flex justify-between" x-show="order?.rejected_at">
|
||||
<span class="text-gray-500 dark:text-gray-400">Declined At</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(order?.rejected_at)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Information -->
|
||||
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||
<span x-html="$icon('user', 'w-5 h-5')"></span>
|
||||
Customer Information
|
||||
</h4>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Name</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="order?.customer_name || 'N/A'"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Email</span>
|
||||
<a :href="'mailto:' + order?.customer_email" class="text-purple-600 hover:underline" x-text="order?.customer_email"></a>
|
||||
</div>
|
||||
<div class="flex justify-between" x-show="order?.customer_locale">
|
||||
<span class="text-gray-500 dark:text-gray-400">Language</span>
|
||||
<span class="uppercase font-medium text-gray-700 dark:text-gray-300" x-text="order?.customer_locale"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Address -->
|
||||
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800" x-show="shippingAddress">
|
||||
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||
<span x-html="$icon('location-marker', 'w-5 h-5')"></span>
|
||||
Shipping Address
|
||||
</h4>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<p x-text="shippingAddress?.firstName + ' ' + shippingAddress?.lastName"></p>
|
||||
<p x-show="shippingAddress?.company" x-text="shippingAddress?.company"></p>
|
||||
<p x-text="shippingAddress?.streetName + ' ' + shippingAddress?.streetNumber"></p>
|
||||
<p x-text="shippingAddress?.zipCode + ' ' + shippingAddress?.city"></p>
|
||||
<p x-text="shippingAddress?.country?.name || order?.shipping_country_iso"></p>
|
||||
<p x-show="shippingAddress?.phone" class="mt-2">
|
||||
<span class="text-gray-500 dark:text-gray-400">Phone:</span>
|
||||
<span x-text="shippingAddress?.phone"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tracking Information -->
|
||||
<div class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||
<span x-html="$icon('truck', 'w-5 h-5')"></span>
|
||||
Tracking Information
|
||||
</h4>
|
||||
<div x-show="order?.tracking_number" class="space-y-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Carrier</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300" x-text="order?.tracking_carrier"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Tracking Number</span>
|
||||
<span class="font-mono text-gray-700 dark:text-gray-300" x-text="order?.tracking_number"></span>
|
||||
</div>
|
||||
<div class="flex justify-between" x-show="order?.tracking_set_at">
|
||||
<span class="text-gray-500 dark:text-gray-400">Set At</span>
|
||||
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(order?.tracking_set_at)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="!order?.tracking_number" class="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
No tracking information available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div x-show="order && !loading" class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 mb-8">
|
||||
<h4 class="mb-4 font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||
<span x-html="$icon('shopping-bag', 'w-5 h-5')"></span>
|
||||
Order Items
|
||||
<span class="text-sm font-normal text-gray-500">
|
||||
(<span x-text="order?.inventory_units?.length || 0"></span> items)
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full whitespace-nowrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Product</th>
|
||||
<th class="px-4 py-3">EAN/SKU</th>
|
||||
<th class="px-4 py-3">Price</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="unit in order?.inventory_units || []" :key="unit.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<!-- Placeholder for product image -->
|
||||
<div class="w-10 h-10 rounded bg-gray-200 dark:bg-gray-600 flex items-center justify-center mr-3">
|
||||
<span x-html="$icon('photograph', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-800 dark:text-gray-200" x-text="unit.product_name || 'Unknown Product'"></p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" x-show="unit.mpn">
|
||||
MPN: <span x-text="unit.mpn"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<p x-show="unit.ean" class="font-mono">
|
||||
<span x-text="unit.ean"></span>
|
||||
<span x-show="unit.ean_type" class="text-xs text-gray-400" x-text="'(' + unit.ean_type + ')'"></span>
|
||||
</p>
|
||||
<p x-show="unit.sku" class="text-xs text-gray-500">
|
||||
SKU: <span x-text="unit.sku"></span>
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm font-medium">
|
||||
<span x-text="unit.price ? (unit.price + ' EUR') : 'N/A'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="px-2 py-1 text-xs rounded-full font-medium"
|
||||
:class="{
|
||||
'bg-orange-100 text-orange-700': unit.state === 'unconfirmed',
|
||||
'bg-green-100 text-green-700': unit.state === 'confirmed_available' || unit.state === 'confirmed',
|
||||
'bg-red-100 text-red-700': unit.state === 'confirmed_unavailable',
|
||||
'bg-gray-100 text-gray-700': unit.state === 'returned'
|
||||
}"
|
||||
x-text="unit.state === 'confirmed_unavailable' ? 'DECLINED' : (unit.state === 'confirmed_available' ? 'CONFIRMED' : unit.state?.toUpperCase())"
|
||||
></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Order Data (collapsible) -->
|
||||
<div x-show="order && !loading" class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 mb-8">
|
||||
<button
|
||||
@click="showRawData = !showRawData"
|
||||
class="w-full flex items-center justify-between text-left"
|
||||
>
|
||||
<h4 class="font-semibold text-gray-600 dark:text-gray-300 flex items-center gap-2">
|
||||
<span x-html="$icon('code', 'w-5 h-5')"></span>
|
||||
Raw Order Data
|
||||
</h4>
|
||||
<span x-html="showRawData ? $icon('chevron-up', 'w-5 h-5 text-gray-500') : $icon('chevron-down', 'w-5 h-5 text-gray-500')"></span>
|
||||
</button>
|
||||
<div x-show="showRawData" class="mt-4">
|
||||
<pre class="text-xs bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-x-auto max-h-96"><code x-text="JSON.stringify(order?.raw_order_data, null, 2)"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function letzshopOrderDetail() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'marketplace-letzshop',
|
||||
orderId: {{ order_id }},
|
||||
order: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
showRawData: false,
|
||||
|
||||
get shippingAddress() {
|
||||
return this.order?.raw_order_data?.order?.shipAddress;
|
||||
},
|
||||
|
||||
get billingAddress() {
|
||||
return this.order?.raw_order_data?.order?.billAddress;
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.loadOrder();
|
||||
},
|
||||
|
||||
async loadOrder() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
// First, we need to find which vendor this order belongs to
|
||||
// We'll fetch the order detail from the API
|
||||
const response = await apiClient.get(`/admin/letzshop/orders/${this.orderId}`);
|
||||
this.order = response;
|
||||
} catch (err) {
|
||||
console.error('Failed to load order:', err);
|
||||
this.error = err.message || 'Failed to load order details';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
return new Date(dateString).toLocaleString();
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -252,18 +252,33 @@
|
||||
@click.stop
|
||||
>
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Order Details</h3>
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Order Details</h3>
|
||||
<a
|
||||
:href="'/admin/letzshop/orders/' + selectedOrder?.id"
|
||||
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400 flex items-center gap-1"
|
||||
title="Open full order page"
|
||||
>
|
||||
<span x-html="$icon('external-link', 'w-3 h-3')"></span>
|
||||
Full View
|
||||
</a>
|
||||
</div>
|
||||
<button @click="showOrderModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<span x-html="$icon('x', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div x-show="selectedOrder" class="space-y-4">
|
||||
<!-- Order Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Order Number:</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300" x-text="selectedOrder?.letzshop_order_number"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Order Date:</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300" x-text="formatDate(selectedOrder?.order_date || selectedOrder?.created_at)"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Status:</span>
|
||||
<span
|
||||
@@ -277,29 +292,67 @@
|
||||
x-text="selectedOrder?.sync_status === 'rejected' ? 'DECLINED' : selectedOrder?.sync_status?.toUpperCase()"
|
||||
></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Customer:</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300" x-text="selectedOrder?.customer_email"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Total:</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300" x-text="selectedOrder?.total_amount + ' ' + selectedOrder?.currency"></span>
|
||||
</div>
|
||||
<div x-show="selectedOrder?.tracking_number">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Tracking:</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300" x-text="selectedOrder?.tracking_number + ' (' + selectedOrder?.tracking_carrier + ')'"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Created:</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-300" x-text="formatDate(selectedOrder?.created_at)"></span>
|
||||
</div>
|
||||
|
||||
<!-- Customer & Shipping Info -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<span x-html="$icon('user', 'w-4 h-4')"></span>
|
||||
Customer & Shipping
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Name:</span>
|
||||
<span class="ml-1" x-text="selectedOrder?.customer_name || 'N/A'"></span>
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Email:</span>
|
||||
<a :href="'mailto:' + selectedOrder?.customer_email" class="ml-1 text-purple-600 hover:underline" x-text="selectedOrder?.customer_email"></a>
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-400" x-show="selectedOrder?.customer_locale">
|
||||
<span class="font-medium">Language:</span>
|
||||
<span class="ml-1 uppercase" x-text="selectedOrder?.customer_locale"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div x-show="selectedOrder?.shipping_country_iso">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Ship to:</span>
|
||||
<span class="ml-1" x-text="selectedOrder?.shipping_country_iso"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="selectedOrder?.inventory_units?.length > 0">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<!-- Tracking Info -->
|
||||
<div x-show="selectedOrder?.tracking_number" class="border-t border-gray-200 dark:border-gray-600 pt-4">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<span x-html="$icon('truck', 'w-4 h-4')"></span>
|
||||
Tracking
|
||||
</h4>
|
||||
<div class="text-sm bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
<span class="font-medium">Carrier:</span>
|
||||
<span class="ml-1" x-text="selectedOrder?.tracking_carrier"></span>
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
<span class="font-medium">Tracking #:</span>
|
||||
<span class="ml-1 font-mono" x-text="selectedOrder?.tracking_number"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div x-show="selectedOrder?.inventory_units?.length > 0" class="border-t border-gray-200 dark:border-gray-600 pt-4">
|
||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<span x-html="$icon('shopping-bag', 'w-4 h-4')"></span>
|
||||
Items
|
||||
<span class="text-xs font-normal text-gray-500">
|
||||
(Each item must be confirmed or declined individually)
|
||||
(<span x-text="selectedOrder?.inventory_units?.length"></span> item<span x-show="selectedOrder?.inventory_units?.length > 1">s</span>)
|
||||
</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/templates/admin/orders.html #}
|
||||
{% extends "admin/base.html" %}
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{% from 'shared/macros/headers.html' import page_header %}
|
||||
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
||||
{% from 'shared/macros/tables.html' import table_wrapper %}
|
||||
{% from 'shared/macros/modals.html' import modal_simple %}
|
||||
@@ -11,8 +11,79 @@
|
||||
|
||||
{% block alpine_data %}adminOrders(){% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Tom Select CSS with local fallback -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/css/tom-select.default.min.css"
|
||||
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/vendor/tom-select.default.min.css') }}';"
|
||||
/>
|
||||
<style>
|
||||
/* Tom Select dark mode overrides */
|
||||
.dark .ts-wrapper .ts-control {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-wrapper .ts-control input::placeholder {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
.dark .ts-dropdown {
|
||||
background-color: rgb(55 65 81);
|
||||
border-color: rgb(75 85 99);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option {
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
.dark .ts-dropdown .option.active {
|
||||
background-color: rgb(147 51 234);
|
||||
color: white;
|
||||
}
|
||||
.dark .ts-dropdown .option:hover {
|
||||
background-color: rgb(75 85 99);
|
||||
}
|
||||
.dark .ts-wrapper.focus .ts-control {
|
||||
border-color: rgb(147 51 234);
|
||||
box-shadow: 0 0 0 1px rgb(147 51 234);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header('Orders', subtitle='Manage orders across all vendors') }}
|
||||
<!-- Page Header with Vendor Selector -->
|
||||
{% call page_header_flex(title='Orders', subtitle='Manage orders across all vendors') %}
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Vendor Autocomplete (Tom Select) -->
|
||||
<div class="w-80">
|
||||
<select id="vendor-select" x-ref="vendorSelect" placeholder="All vendors...">
|
||||
</select>
|
||||
</div>
|
||||
{{ refresh_button(loading_var='loading', onclick='refresh()', variant='secondary') }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Selected Vendor Info (optional display) -->
|
||||
<div x-show="selectedVendor" x-transition class="mb-6 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
|
||||
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="selectedVendor?.name?.charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-purple-800 dark:text-purple-200" x-text="selectedVendor?.name"></span>
|
||||
<span class="ml-2 text-xs text-purple-600 dark:text-purple-400 font-mono" x-text="selectedVendor?.vendor_code"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="clearVendorFilter()" class="text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-sm flex items-center gap-1">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ loading_state('Loading orders...') }}
|
||||
|
||||
@@ -102,14 +173,6 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Vendor Filter (Tom Select) -->
|
||||
{{ vendor_selector(
|
||||
ref_name='vendorSelect',
|
||||
id='orders-vendor-select',
|
||||
placeholder='Filter by vendor...',
|
||||
width='w-64'
|
||||
) }}
|
||||
|
||||
<!-- Status Filter -->
|
||||
<select
|
||||
x-model="filters.status"
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
@click="importHistoricalOrders()"
|
||||
:disabled="!letzshopStatus.is_configured || importingHistorical"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Import all historical confirmed orders"
|
||||
title="Import all historical confirmed and declined orders"
|
||||
>
|
||||
<span x-show="!importingHistorical" x-html="$icon('archive', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="importingHistorical" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="importingHistorical ? 'Importing...' : 'Import History'"></span>
|
||||
<span x-show="!importingHistorical">Import History</span>
|
||||
<span x-show="importingHistorical" x-text="historicalImportProgress?.message || 'Starting...'"></span>
|
||||
</button>
|
||||
<button
|
||||
@click="importOrders()"
|
||||
@@ -30,6 +31,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Import Progress -->
|
||||
<div x-show="historicalImportProgress && importingHistorical" x-transition class="mb-6 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('spinner', 'w-5 h-5 text-purple-500 mr-3')"></span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-purple-800 dark:text-purple-200">Historical Import in Progress</h4>
|
||||
<p class="text-sm text-purple-700 dark:text-purple-300 mt-1" x-text="historicalImportProgress?.message"></p>
|
||||
<div class="flex gap-4 mt-2 text-xs text-purple-600 dark:text-purple-400">
|
||||
<span x-show="historicalImportProgress?.current_phase">
|
||||
Phase: <strong x-text="historicalImportProgress?.current_phase"></strong>
|
||||
</span>
|
||||
<span x-show="historicalImportProgress?.shipments_fetched > 0">
|
||||
Fetched: <strong x-text="historicalImportProgress?.shipments_fetched"></strong>
|
||||
</span>
|
||||
<span x-show="historicalImportProgress?.orders_processed > 0">
|
||||
Processed: <strong x-text="historicalImportProgress?.orders_processed"></strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Import Result -->
|
||||
<div x-show="historicalImportResult" x-transition class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-start justify-between">
|
||||
@@ -113,10 +136,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 flex flex-wrap gap-4">
|
||||
<div class="mb-4 flex flex-wrap gap-4 items-center">
|
||||
<!-- Search input -->
|
||||
<div class="relative flex-1 min-w-[200px] max-w-md">
|
||||
<span x-html="$icon('search', 'w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400')"></span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="ordersSearch"
|
||||
@input.debounce.300ms="ordersPage = 1; loadOrders()"
|
||||
placeholder="Search by order #, name, or email..."
|
||||
class="w-full pl-9 pr-8 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
x-show="ordersSearch"
|
||||
@click="ordersSearch = ''; ordersPage = 1; loadOrders()"
|
||||
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status filter -->
|
||||
<select
|
||||
x-model="ordersFilter"
|
||||
@change="loadOrders()"
|
||||
@change="ordersHasDeclinedItems = false; ordersPage = 1; loadOrders()"
|
||||
class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
@@ -125,6 +168,18 @@
|
||||
<option value="rejected">Declined</option>
|
||||
<option value="shipped">Shipped</option>
|
||||
</select>
|
||||
|
||||
<!-- Declined items filter -->
|
||||
<button
|
||||
@click="ordersFilter = ''; ordersHasDeclinedItems = !ordersHasDeclinedItems; ordersPage = 1; loadOrders()"
|
||||
:class="ordersHasDeclinedItems ? 'bg-red-100 dark:bg-red-900 border-red-300 dark:border-red-700 text-red-700 dark:text-red-300' : 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300'"
|
||||
class="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
:title="ordersHasDeclinedItems ? 'Showing orders with declined items' : 'Show only orders with declined items'"
|
||||
>
|
||||
<span x-html="$icon('x-circle', 'w-4 h-4 inline mr-1')"></span>
|
||||
Has Declined Items
|
||||
<span x-show="orderStats.has_declined_items > 0" class="ml-1 px-1.5 py-0.5 text-xs bg-red-200 dark:bg-red-800 rounded-full" x-text="orderStats.has_declined_items"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Not Configured Warning -->
|
||||
@@ -199,7 +254,7 @@
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="formatDate(order.created_at)"></span>
|
||||
<span x-text="formatDate(order.order_date || order.created_at)"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
|
||||
@@ -35,8 +35,11 @@ function adminMarketplaceLetzshop() {
|
||||
testingConnection: false,
|
||||
submittingTracking: false,
|
||||
|
||||
// Historical import result
|
||||
// Historical import state
|
||||
historicalImportResult: null,
|
||||
historicalImportJobId: null,
|
||||
historicalImportProgress: null,
|
||||
historicalImportPollInterval: null,
|
||||
|
||||
// Messages
|
||||
error: '',
|
||||
@@ -87,7 +90,9 @@ function adminMarketplaceLetzshop() {
|
||||
ordersPage: 1,
|
||||
ordersLimit: 20,
|
||||
ordersFilter: '',
|
||||
orderStats: { pending: 0, confirmed: 0, rejected: 0, shipped: 0 },
|
||||
ordersSearch: '',
|
||||
ordersHasDeclinedItems: false,
|
||||
orderStats: { pending: 0, confirmed: 0, rejected: 0, shipped: 0, has_declined_items: 0 },
|
||||
|
||||
// Jobs
|
||||
jobs: [],
|
||||
@@ -226,6 +231,9 @@ function adminMarketplaceLetzshop() {
|
||||
this.letzshopStatus = { is_configured: false };
|
||||
this.credentials = null;
|
||||
this.orders = [];
|
||||
this.ordersFilter = '';
|
||||
this.ordersSearch = '';
|
||||
this.ordersHasDeclinedItems = false;
|
||||
this.jobs = [];
|
||||
this.settingsForm = {
|
||||
api_key: '',
|
||||
@@ -394,6 +402,14 @@ function adminMarketplaceLetzshop() {
|
||||
params.append('sync_status', this.ordersFilter);
|
||||
}
|
||||
|
||||
if (this.ordersHasDeclinedItems) {
|
||||
params.append('has_declined_items', 'true');
|
||||
}
|
||||
|
||||
if (this.ordersSearch) {
|
||||
params.append('search', this.ordersSearch);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(`/admin/letzshop/vendors/${this.selectedVendor.id}/orders?${params}`);
|
||||
this.orders = response.orders || [];
|
||||
this.totalOrders = response.total || 0;
|
||||
@@ -421,7 +437,7 @@ function adminMarketplaceLetzshop() {
|
||||
*/
|
||||
updateOrderStats() {
|
||||
// Reset stats
|
||||
this.orderStats = { pending: 0, confirmed: 0, rejected: 0, shipped: 0 };
|
||||
this.orderStats = { pending: 0, confirmed: 0, rejected: 0, shipped: 0, has_declined_items: 0 };
|
||||
|
||||
// Count from orders list (only visible page - not accurate for totals)
|
||||
for (const order of this.orders) {
|
||||
@@ -455,6 +471,7 @@ function adminMarketplaceLetzshop() {
|
||||
|
||||
/**
|
||||
* Import historical orders from Letzshop (confirmed and declined orders)
|
||||
* Uses background job with polling for progress tracking
|
||||
*/
|
||||
async importHistoricalOrders() {
|
||||
if (!this.selectedVendor || !this.letzshopStatus.is_configured) return;
|
||||
@@ -463,44 +480,154 @@ function adminMarketplaceLetzshop() {
|
||||
this.error = '';
|
||||
this.successMessage = '';
|
||||
this.historicalImportResult = null;
|
||||
this.historicalImportProgress = {
|
||||
status: 'starting',
|
||||
message: 'Starting historical import...',
|
||||
current_phase: null,
|
||||
current_page: 0,
|
||||
shipments_fetched: 0,
|
||||
orders_processed: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
// Import confirmed orders
|
||||
const confirmedResponse = await apiClient.post(
|
||||
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history?state=confirmed`
|
||||
// Start the import job
|
||||
const response = await apiClient.post(
|
||||
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history`
|
||||
);
|
||||
const confirmedStats = confirmedResponse.statistics || confirmedResponse;
|
||||
|
||||
// Import declined (rejected) orders
|
||||
const declinedResponse = await apiClient.post(
|
||||
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history?state=declined`
|
||||
);
|
||||
const declinedStats = declinedResponse.statistics || declinedResponse;
|
||||
this.historicalImportJobId = response.job_id;
|
||||
marketplaceLetzshopLog.info('Historical import job started:', response);
|
||||
|
||||
// Combine stats
|
||||
this.historicalImportResult = {
|
||||
imported: (confirmedStats.imported || 0) + (declinedStats.imported || 0),
|
||||
updated: (confirmedStats.updated || 0) + (declinedStats.updated || 0),
|
||||
skipped: (confirmedStats.skipped || 0) + (declinedStats.skipped || 0),
|
||||
products_matched: (confirmedStats.products_matched || 0) + (declinedStats.products_matched || 0),
|
||||
products_not_found: (confirmedStats.products_not_found || 0) + (declinedStats.products_not_found || 0),
|
||||
};
|
||||
const stats = this.historicalImportResult;
|
||||
this.successMessage = `Historical import complete: ${stats.imported} imported, ${stats.updated} updated`;
|
||||
// Start polling for progress
|
||||
this.startHistoricalImportPolling();
|
||||
|
||||
marketplaceLetzshopLog.info('Historical import result (confirmed):', confirmedResponse);
|
||||
marketplaceLetzshopLog.info('Historical import result (declined):', declinedResponse);
|
||||
|
||||
// Reload orders to show new data
|
||||
await this.loadOrders();
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to import historical orders:', error);
|
||||
this.error = error.message || 'Failed to import historical orders';
|
||||
} finally {
|
||||
marketplaceLetzshopLog.error('Failed to start historical import:', error);
|
||||
this.error = error.message || 'Failed to start historical import';
|
||||
this.importingHistorical = false;
|
||||
this.historicalImportProgress = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start polling for historical import progress
|
||||
*/
|
||||
startHistoricalImportPolling() {
|
||||
// Poll every 2 seconds
|
||||
this.historicalImportPollInterval = setInterval(async () => {
|
||||
await this.pollHistoricalImportStatus();
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Poll historical import status
|
||||
*/
|
||||
async pollHistoricalImportStatus() {
|
||||
if (!this.historicalImportJobId || !this.selectedVendor) {
|
||||
this.stopHistoricalImportPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await apiClient.get(
|
||||
`/admin/letzshop/vendors/${this.selectedVendor.id}/import-history/${this.historicalImportJobId}/status`
|
||||
);
|
||||
|
||||
// Update progress display
|
||||
this.historicalImportProgress = {
|
||||
status: status.status,
|
||||
message: this.formatProgressMessage(status),
|
||||
current_phase: status.current_phase,
|
||||
current_page: status.current_page,
|
||||
total_pages: status.total_pages,
|
||||
shipments_fetched: status.shipments_fetched,
|
||||
orders_processed: status.orders_processed,
|
||||
};
|
||||
|
||||
// Check if complete or failed
|
||||
if (status.status === 'completed' || status.status === 'failed') {
|
||||
this.stopHistoricalImportPolling();
|
||||
this.importingHistorical = false;
|
||||
|
||||
if (status.status === 'completed') {
|
||||
// Combine stats from both phases
|
||||
const confirmed = status.confirmed_stats || {};
|
||||
const pending = status.declined_stats || {}; // Actually unconfirmed/pending
|
||||
|
||||
this.historicalImportResult = {
|
||||
imported: (confirmed.imported || 0) + (pending.imported || 0),
|
||||
updated: (confirmed.updated || 0) + (pending.updated || 0),
|
||||
skipped: (confirmed.skipped || 0) + (pending.skipped || 0),
|
||||
products_matched: (confirmed.products_matched || 0) + (pending.products_matched || 0),
|
||||
products_not_found: (confirmed.products_not_found || 0) + (pending.products_not_found || 0),
|
||||
};
|
||||
const stats = this.historicalImportResult;
|
||||
|
||||
// Build a meaningful summary message
|
||||
const parts = [];
|
||||
if (stats.imported > 0) parts.push(`${stats.imported} imported`);
|
||||
if (stats.updated > 0) parts.push(`${stats.updated} updated`);
|
||||
if (stats.skipped > 0) parts.push(`${stats.skipped} already synced`);
|
||||
|
||||
this.successMessage = parts.length > 0
|
||||
? `Historical import complete: ${parts.join(', ')}`
|
||||
: 'Historical import complete: no orders found';
|
||||
marketplaceLetzshopLog.info('Historical import completed:', status);
|
||||
|
||||
// Reload orders to show new data
|
||||
await this.loadOrders();
|
||||
} else {
|
||||
this.error = status.error_message || 'Historical import failed';
|
||||
marketplaceLetzshopLog.error('Historical import failed:', status);
|
||||
}
|
||||
|
||||
this.historicalImportProgress = null;
|
||||
this.historicalImportJobId = null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
marketplaceLetzshopLog.error('Failed to poll import status:', error);
|
||||
// Don't stop polling on transient errors
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop polling for historical import progress
|
||||
*/
|
||||
stopHistoricalImportPolling() {
|
||||
if (this.historicalImportPollInterval) {
|
||||
clearInterval(this.historicalImportPollInterval);
|
||||
this.historicalImportPollInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Format progress message for display
|
||||
*/
|
||||
formatProgressMessage(status) {
|
||||
// Map phase to display name
|
||||
const phaseNames = {
|
||||
'confirmed': 'confirmed',
|
||||
'unconfirmed': 'pending',
|
||||
'declined': 'declined', // Legacy support
|
||||
};
|
||||
const phase = phaseNames[status.current_phase] || status.current_phase || 'orders';
|
||||
|
||||
if (status.status === 'fetching') {
|
||||
if (status.total_pages) {
|
||||
return `Fetching ${phase} orders: page ${status.current_page} of ${status.total_pages} (${status.shipments_fetched} fetched)`;
|
||||
}
|
||||
return `Fetching ${phase} orders: page ${status.current_page}... (${status.shipments_fetched} fetched)`;
|
||||
}
|
||||
if (status.status === 'processing') {
|
||||
return `Processing ${phase} orders: ${status.orders_processed} processed...`;
|
||||
}
|
||||
if (status.status === 'pending') {
|
||||
return 'Starting historical import...';
|
||||
}
|
||||
return status.status.charAt(0).toUpperCase() + status.status.slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm an order
|
||||
*/
|
||||
|
||||
@@ -49,6 +49,12 @@ function adminOrders() {
|
||||
// Available vendors for filter dropdown
|
||||
vendors: [],
|
||||
|
||||
// Selected vendor (for prominent display)
|
||||
selectedVendor: null,
|
||||
|
||||
// Tom Select instance
|
||||
vendorSelectInstance: null,
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
page: 1,
|
||||
@@ -128,6 +134,9 @@ function adminOrders() {
|
||||
}
|
||||
window._adminOrdersInitialized = true;
|
||||
|
||||
// Initialize Tom Select for vendor filter
|
||||
this.initVendorSelect();
|
||||
|
||||
// Load data in parallel
|
||||
await Promise.all([
|
||||
this.loadStats(),
|
||||
@@ -138,6 +147,82 @@ function adminOrders() {
|
||||
adminOrdersLog.info('Orders initialization complete');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize Tom Select for vendor autocomplete
|
||||
*/
|
||||
initVendorSelect() {
|
||||
const selectEl = this.$refs.vendorSelect;
|
||||
if (!selectEl) {
|
||||
adminOrdersLog.warn('Vendor select element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for Tom Select to be available
|
||||
if (typeof TomSelect === 'undefined') {
|
||||
adminOrdersLog.warn('TomSelect not loaded, retrying in 100ms');
|
||||
setTimeout(() => this.initVendorSelect(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
this.vendorSelectInstance = new TomSelect(selectEl, {
|
||||
valueField: 'id',
|
||||
labelField: 'name',
|
||||
searchField: ['name', 'vendor_code'],
|
||||
placeholder: 'All vendors...',
|
||||
allowEmptyOption: true,
|
||||
load: async (query, callback) => {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/vendors', {
|
||||
search: query,
|
||||
limit: 50
|
||||
});
|
||||
callback(response.vendors || []);
|
||||
} catch (error) {
|
||||
adminOrdersLog.error('Failed to search vendors:', error);
|
||||
callback([]);
|
||||
}
|
||||
},
|
||||
render: {
|
||||
option: (data, escape) => {
|
||||
return `<div class="flex items-center justify-between py-1">
|
||||
<span>${escape(data.name)}</span>
|
||||
<span class="text-xs text-gray-400 font-mono">${escape(data.vendor_code || '')}</span>
|
||||
</div>`;
|
||||
},
|
||||
item: (data, escape) => {
|
||||
return `<div>${escape(data.name)}</div>`;
|
||||
}
|
||||
},
|
||||
onChange: (value) => {
|
||||
if (value) {
|
||||
const vendor = this.vendorSelectInstance.options[value];
|
||||
this.selectedVendor = vendor;
|
||||
this.filters.vendor_id = value;
|
||||
} else {
|
||||
this.selectedVendor = null;
|
||||
this.filters.vendor_id = '';
|
||||
}
|
||||
this.pagination.page = 1;
|
||||
this.loadOrders();
|
||||
}
|
||||
});
|
||||
|
||||
adminOrdersLog.info('Vendor select initialized');
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear vendor filter
|
||||
*/
|
||||
clearVendorFilter() {
|
||||
if (this.vendorSelectInstance) {
|
||||
this.vendorSelectInstance.clear();
|
||||
}
|
||||
this.selectedVendor = null;
|
||||
this.filters.vendor_id = '';
|
||||
this.pagination.page = 1;
|
||||
this.loadOrders();
|
||||
},
|
||||
|
||||
/**
|
||||
* Load order statistics
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user