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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user