feat: redesign Letzshop products tab with product listing view

Products Tab Changes:
- Converted to product listing page similar to /admin/marketplace-products
- Added Import/Export buttons in header
- Added product stats cards (total, active, inactive, last sync)
- Added search and filter functionality
- Added product table with pagination
- Import modal for single URL or all languages

Settings Tab Changes:
- Moved batch size setting from products tab
- Moved include inactive checkbox from products tab
- Added export behavior info box

Export Changes:
- New POST endpoint exports all languages (FR, DE, EN)
- CSV files written to exports/letzshop/{vendor_code}/ for scheduler pickup
- Letzshop scheduler can fetch files from this location

API Changes:
- Added vendor_id filter to /admin/vendor-products/stats endpoint
- Added POST /admin/vendors/{id}/export/letzshop for folder export

🤖 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-20 21:44:59 +01:00
parent 44c11181fd
commit d46b676e77
6 changed files with 692 additions and 258 deletions

View File

@@ -75,12 +75,24 @@ class VendorProductService:
return result, total
def get_product_stats(self, db: Session) -> dict:
"""Get vendor product statistics for admin dashboard."""
total = db.query(func.count(Product.id)).scalar() or 0
def get_product_stats(self, db: Session, vendor_id: int | None = None) -> dict:
"""Get vendor product statistics for admin dashboard.
Args:
db: Database session
vendor_id: Optional vendor ID to filter stats
Returns:
Dict with product counts (total, active, inactive, etc.)
"""
# Base query filter
base_filter = Product.vendor_id == vendor_id if vendor_id else True
total = db.query(func.count(Product.id)).filter(base_filter).scalar() or 0
active = (
db.query(func.count(Product.id))
.filter(base_filter)
.filter(Product.is_active == True) # noqa: E712
.scalar()
or 0
@@ -89,6 +101,7 @@ class VendorProductService:
featured = (
db.query(func.count(Product.id))
.filter(base_filter)
.filter(Product.is_featured == True) # noqa: E712
.scalar()
or 0
@@ -97,6 +110,7 @@ class VendorProductService:
# Digital/physical counts
digital = (
db.query(func.count(Product.id))
.filter(base_filter)
.join(Product.marketplace_product)
.filter(Product.marketplace_product.has(is_digital=True))
.scalar()
@@ -104,17 +118,19 @@ class VendorProductService:
)
physical = total - digital
# Count by vendor
vendor_counts = (
db.query(
Vendor.name,
func.count(Product.id),
# Count by vendor (only when not filtered by vendor_id)
by_vendor = {}
if not vendor_id:
vendor_counts = (
db.query(
Vendor.name,
func.count(Product.id),
)
.join(Vendor, Product.vendor_id == Vendor.id)
.group_by(Vendor.name)
.all()
)
.join(Vendor, Product.vendor_id == Vendor.id)
.group_by(Vendor.name)
.all()
)
by_vendor = {name or "unknown": count for name, count in vendor_counts}
by_vendor = {name or "unknown": count for name, count in vendor_counts}
return {
"total": total,