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

@@ -261,6 +261,126 @@ class CustomerService:
.first()
)
def get_vendor_customers(
self,
db: Session,
vendor_id: int,
skip: int = 0,
limit: int = 100,
search: str | None = None,
is_active: bool | None = None,
) -> tuple[list[Customer], int]:
"""
Get all customers for a vendor with filtering and pagination.
Args:
db: Database session
vendor_id: Vendor ID
skip: Pagination offset
limit: Pagination limit
search: Search in name/email
is_active: Filter by active status
Returns:
Tuple of (customers, total_count)
"""
from sqlalchemy import or_
query = db.query(Customer).filter(Customer.vendor_id == vendor_id)
if search:
search_pattern = f"%{search}%"
query = query.filter(
or_(
Customer.email.ilike(search_pattern),
Customer.first_name.ilike(search_pattern),
Customer.last_name.ilike(search_pattern),
Customer.customer_number.ilike(search_pattern),
)
)
if is_active is not None:
query = query.filter(Customer.is_active == is_active)
# Order by most recent first
query = query.order_by(Customer.created_at.desc())
total = query.count()
customers = query.offset(skip).limit(limit).all()
return customers, total
def get_customer_statistics(
self, db: Session, vendor_id: int, customer_id: int
) -> dict:
"""
Get detailed statistics for a customer.
Args:
db: Database session
vendor_id: Vendor ID
customer_id: Customer ID
Returns:
Dict with customer statistics
"""
from sqlalchemy import func
from models.database.order import Order
customer = self.get_customer(db, vendor_id, customer_id)
# Get order statistics
order_stats = (
db.query(
func.count(Order.id).label("total_orders"),
func.sum(Order.total_cents).label("total_spent_cents"),
func.avg(Order.total_cents).label("avg_order_cents"),
func.max(Order.created_at).label("last_order_date"),
)
.filter(Order.customer_id == customer_id)
.first()
)
total_orders = order_stats.total_orders or 0
total_spent_cents = order_stats.total_spent_cents or 0
avg_order_cents = order_stats.avg_order_cents or 0
return {
"customer_id": customer_id,
"total_orders": total_orders,
"total_spent": total_spent_cents / 100, # Convert to euros
"average_order_value": avg_order_cents / 100 if avg_order_cents else 0.0,
"last_order_date": order_stats.last_order_date,
"member_since": customer.created_at,
"is_active": customer.is_active,
}
def toggle_customer_status(
self, db: Session, vendor_id: int, customer_id: int
) -> Customer:
"""
Toggle customer active status.
Args:
db: Database session
vendor_id: Vendor ID
customer_id: Customer ID
Returns:
Customer: Updated customer
"""
customer = self.get_customer(db, vendor_id, customer_id)
customer.is_active = not customer.is_active
db.flush()
db.refresh(customer)
action = "activated" if customer.is_active else "deactivated"
logger.info(f"Customer {action}: {customer.email} (ID: {customer.id})")
return customer
def update_customer(
self,
db: Session,