feat: show all Letzshop products in Products tab without vendor filter

- Update loadProducts() to show all Letzshop marketplace products when
  no vendor is selected, filtered by vendor when one is selected
- Add vendor column to products table (shown when no vendor filter)
- Update header to show context-aware subtitle
- Hide Import/Export buttons when no vendor selected
- Remove "Marketplace Products" from sidebar menu (accessible via
  Marketplace > Letzshop > Products tab)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 00:07:27 +01:00
parent 508e121a0e
commit 1a962dc6d1
3 changed files with 33 additions and 22 deletions

View File

@@ -7,10 +7,13 @@
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Letzshop Products</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Vendor products synced with Letzshop marketplace</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
<span x-show="selectedVendor" x-text="'Products from ' + (selectedVendor?.name || '')"></span>
<span x-show="!selectedVendor">All Letzshop marketplace products</span>
</p>
</div>
<div class="flex items-center gap-3">
<!-- Import Button -->
<div class="flex items-center gap-3" x-show="selectedVendor">
<!-- Import Button (only when vendor selected) -->
<button
@click="showImportModal = true"
:disabled="importing"
@@ -20,7 +23,7 @@
<span x-show="importing" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="importing ? 'Importing...' : 'Import'"></span>
</button>
<!-- Export Button -->
<!-- Export Button (only when vendor selected) -->
<button
@click="exportAllLanguages()"
:disabled="exporting"
@@ -138,6 +141,7 @@
<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" x-show="!selectedVendor">Vendor</th>
<th class="px-4 py-3">Identifiers</th>
<th class="px-4 py-3">Price</th>
<th class="px-4 py-3">Status</th>
@@ -148,11 +152,11 @@
<!-- Empty State -->
<template x-if="products.length === 0">
<tr>
<td colspan="5" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
<td :colspan="selectedVendor ? 5 : 6" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('cube', 'w-12 h-12 mb-2 text-gray-300')"></span>
<p class="font-medium">No products found</p>
<p class="text-xs mt-1" x-text="productFilters.search ? 'Try adjusting your search' : 'Import products to get started'"></p>
<p class="text-xs mt-1" x-text="productFilters.search ? 'Try adjusting your search' : (selectedVendor ? 'Import products to get started' : 'No Letzshop products in the catalog')"></p>
</div>
</td>
</tr>
@@ -183,6 +187,11 @@
</div>
</td>
<!-- Vendor (shown when no vendor filter) -->
<td class="px-4 py-3 text-sm" x-show="!selectedVendor">
<span class="font-medium" x-text="product.vendor_name || '-'"></span>
</td>
<!-- Identifiers -->
<td class="px-4 py-3 text-sm">
<div class="space-y-1">

View File

@@ -78,7 +78,6 @@
<!-- Vendor Operations Section -->
{{ section_header('Vendor Operations', 'vendorOps') }}
{% call section_content('vendorOps') %}
{{ menu_item('marketplace-products', '/admin/marketplace-products', 'database', 'Marketplace Products') }}
{{ menu_item('vendor-products', '/admin/vendor-products', 'cube', 'Vendor Products') }}
{{ menu_item('customers', '/admin/customers', 'user-group', 'Customers') }}
{{ menu_item('inventory', '/admin/inventory', 'archive', 'Inventory') }}

View File

@@ -218,7 +218,7 @@ function adminMarketplaceLetzshop() {
},
/**
* Load cross-vendor aggregate data
* Load cross-vendor aggregate data (when no vendor is selected)
*/
async loadCrossVendorData() {
marketplaceLetzshopLog.info('Loading cross-vendor data');
@@ -226,6 +226,7 @@ function adminMarketplaceLetzshop() {
try {
await Promise.all([
this.loadProducts(),
this.loadOrders(),
this.loadExceptions(),
this.loadExceptionStats(),
@@ -428,27 +429,25 @@ function adminMarketplaceLetzshop() {
// ═══════════════════════════════════════════════════════════════
/**
* Load Letzshop products for selected vendor
* Uses /admin/products endpoint with marketplace=Letzshop filter
* Load Letzshop products
* When vendor is selected: shows products for that vendor
* When no vendor selected: shows ALL Letzshop marketplace products
*/
async loadProducts() {
if (!this.selectedVendor) {
this.products = [];
this.totalProducts = 0;
this.productStats = { total: 0, active: 0, inactive: 0, last_sync: null };
return;
}
this.loadingProducts = true;
try {
const params = new URLSearchParams({
marketplace: 'Letzshop',
vendor_name: this.selectedVendor.name,
skip: ((this.productsPage - 1) * this.productsLimit).toString(),
limit: this.productsLimit.toString()
});
// Filter by vendor if one is selected
if (this.selectedVendor) {
params.append('vendor_name', this.selectedVendor.name);
}
if (this.productFilters.search) {
params.append('search', this.productFilters.search);
}
@@ -474,15 +473,19 @@ function adminMarketplaceLetzshop() {
/**
* Load product statistics for Letzshop products
* Shows stats for selected vendor or all Letzshop products
*/
async loadProductStats() {
if (!this.selectedVendor) return;
try {
const params = new URLSearchParams({
marketplace: 'Letzshop',
vendor_name: this.selectedVendor.name
marketplace: 'Letzshop'
});
// Filter by vendor if one is selected
if (this.selectedVendor) {
params.append('vendor_name', this.selectedVendor.name);
}
const response = await apiClient.get(`/admin/products/stats?${params}`);
this.productStats = {
total: response.total || 0,