feat: implement product search, media library, and vendor customers

- Add full-text product search in ProductService.search_products()
  searching titles, descriptions, SKUs, brands, and GTINs
- Implement complete vendor media library with file uploads,
  thumbnails, folders, and product associations
- Implement vendor customers API with listing, details, orders,
  statistics, and status management
- Add shop search results UI with pagination and add-to-cart
- Add vendor media library UI with drag-drop upload and grid view
- Add database migration for media_files and product_media tables
- Update TODO file with current launch status (~95% complete)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-06 21:32:59 +01:00
parent 4c2f7f1121
commit 8ee3f91467
16 changed files with 2456 additions and 102 deletions

View File

@@ -242,6 +242,89 @@ class ProductService:
logger.error(f"Error getting vendor products: {str(e)}")
raise ValidationException("Failed to retrieve products")
def search_products(
self,
db: Session,
vendor_id: int,
query: str,
skip: int = 0,
limit: int = 50,
language: str = "en",
) -> tuple[list[Product], int]:
"""
Search products in vendor catalog.
Searches across:
- Product title and description (from translations)
- Product SKU, brand, and GTIN
Args:
db: Database session
vendor_id: Vendor ID
query: Search query string
skip: Pagination offset
limit: Pagination limit
language: Language for translation search (default: 'en')
Returns:
Tuple of (products, total_count)
"""
from sqlalchemy import or_
from sqlalchemy.orm import joinedload
from models.database.product_translation import ProductTranslation
try:
# Prepare search pattern for LIKE queries
search_pattern = f"%{query}%"
# Build base query with translation join
base_query = (
db.query(Product)
.outerjoin(
ProductTranslation,
(Product.id == ProductTranslation.product_id)
& (ProductTranslation.language == language),
)
.filter(
Product.vendor_id == vendor_id,
Product.is_active == True,
)
.filter(
or_(
# Search in translations
ProductTranslation.title.ilike(search_pattern),
ProductTranslation.description.ilike(search_pattern),
ProductTranslation.short_description.ilike(search_pattern),
# Search in product fields
Product.vendor_sku.ilike(search_pattern),
Product.brand.ilike(search_pattern),
Product.gtin.ilike(search_pattern),
)
)
.distinct()
)
# Get total count
total = base_query.count()
# Get paginated results with eager loading for performance
products = (
base_query.options(joinedload(Product.translations))
.offset(skip)
.limit(limit)
.all()
)
logger.debug(
f"Search '{query}' for vendor {vendor_id}: {total} results"
)
return products, total
except Exception as e:
logger.error(f"Error searching products: {str(e)}")
raise ValidationException("Failed to search products")
# Create service instance
product_service = ProductService()