feat: enhance Letzshop order import with EAN matching and stats

- Add historical order import with pagination support
- Add customer_locale, shipping_country_iso, billing_country_iso columns
- Add gtin/gtin_type columns to Product table for EAN matching
- Fix order stats to count all orders server-side (not just visible page)
- Add GraphQL introspection script with tracking workaround tests
- Enrich inventory units with EAN, MPN, SKU, product name
- Add LetzshopOrderStats schema for proper status counts

Migrations:
- a9a86cef6cca: Add locale and country fields to letzshop_orders
- cb88bc9b5f86: Add gtin columns to products table

🤖 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-18 21:04:33 +01:00
parent 6d6c8b44d3
commit 0ab10128ae
17 changed files with 3451 additions and 94 deletions

View File

@@ -361,6 +361,9 @@ def list_vendor_letzshop_orders(
sync_status=sync_status,
)
# Get order stats for all statuses
stats = order_service.get_order_stats(vendor_id)
return LetzshopOrderListResponse(
orders=[
LetzshopOrderResponse(
@@ -392,6 +395,7 @@ def list_vendor_letzshop_orders(
total=total,
skip=skip,
limit=limit,
stats=stats,
)
@@ -430,6 +434,10 @@ def trigger_vendor_sync(
try:
with creds_service.create_client(vendor_id) as client:
shipments = client.get_unconfirmed_shipments()
logger.info(
f"Letzshop sync for vendor {vendor_id}: "
f"fetched {len(shipments)} unconfirmed shipments from API"
)
orders_imported = 0
orders_updated = 0
@@ -524,3 +532,118 @@ def list_vendor_letzshop_jobs(
jobs = [LetzshopJobItem(**job) for job in jobs_data]
return LetzshopJobsListResponse(jobs=jobs, total=total)
# ============================================================================
# Historical Import
# ============================================================================
@router.post(
"/vendors/{vendor_id}/import-history",
)
def import_historical_orders(
vendor_id: int = Path(..., description="Vendor ID"),
state: str = Query("confirmed", description="Shipment state to import"),
max_pages: int | None = Query(None, ge=1, le=100, description="Max pages to fetch"),
match_products: bool = Query(True, description="Match EANs to local products"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Import historical orders from Letzshop.
Fetches all shipments with the specified state (default: confirmed)
and imports them into the database. Supports pagination and EAN matching.
Returns statistics on imported/updated/skipped orders and product matching.
"""
order_service = get_order_service(db)
creds_service = get_credentials_service(db)
try:
vendor = order_service.get_vendor_or_raise(vendor_id)
except VendorNotFoundError:
raise ResourceNotFoundException("Vendor", str(vendor_id))
# Verify credentials exist
try:
creds_service.get_credentials_or_raise(vendor_id)
except CredentialsNotFoundError:
raise ValidationException(
f"Letzshop credentials not configured for vendor {vendor.name}"
)
# Fetch all shipments with pagination
try:
with creds_service.create_client(vendor_id) as client:
logger.info(
f"Starting historical import for vendor {vendor_id}, state={state}, max_pages={max_pages}"
)
shipments = client.get_all_shipments_paginated(
state=state,
page_size=50,
max_pages=max_pages,
)
logger.info(f"Fetched {len(shipments)} {state} shipments from Letzshop")
# Import shipments
stats = order_service.import_historical_shipments(
vendor_id=vendor_id,
shipments=shipments,
match_products=match_products,
)
db.commit()
# Update sync status
creds_service.update_sync_status(
vendor_id,
"success",
None,
)
logger.info(
f"Historical import completed: {stats['imported']} imported, "
f"{stats['updated']} updated, {stats['skipped']} skipped"
)
return {
"success": True,
"message": f"Historical import completed: {stats['imported']} imported, {stats['updated']} updated",
"statistics": stats,
}
except LetzshopClientError as e:
creds_service.update_sync_status(vendor_id, "failed", str(e))
raise ValidationException(f"Letzshop API error: {e}")
@router.get(
"/vendors/{vendor_id}/import-summary",
)
def get_import_summary(
vendor_id: int = Path(..., description="Vendor ID"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get summary statistics for imported Letzshop orders.
Returns total orders, unique customers, and breakdowns by state/locale/country.
"""
order_service = get_order_service(db)
try:
order_service.get_vendor_or_raise(vendor_id)
except VendorNotFoundError:
raise ResourceNotFoundException("Vendor", str(vendor_id))
summary = order_service.get_historical_import_summary(vendor_id)
return {
"success": True,
"summary": summary,
}

View File

@@ -8,7 +8,7 @@ for all Letzshop API operations.
import logging
import time
from typing import Any
from typing import Any, Callable
import requests
@@ -43,63 +43,72 @@ class LetzshopConnectionError(LetzshopClientError):
# GraphQL Queries
# ============================================================================
QUERY_SHIPMENTS = """
query GetShipments($state: ShipmentState) {
shipments(state: $state) {
QUERY_SHIPMENTS_UNCONFIRMED = """
query {
shipments(state: unconfirmed) {
nodes {
id
number
state
createdAt
updatedAt
order {
id
number
email
totalPrice {
amount
currency
}
lineItems {
nodes {
id
name
quantity
price {
amount
currency
}
total
completedAt
locale
shipAddress {
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {
name { en fr de }
iso
}
}
shippingAddress {
billAddress {
firstName
lastName
company
address1
address2
streetName
streetNumber
city
zip
country
}
billingAddress {
firstName
lastName
company
address1
address2
city
zip
country
zipCode
phone
country {
name { en fr de }
iso
}
}
}
inventoryUnits {
nodes {
id
state
variant {
id
state
variant {
id
sku
name
sku
mpn
price
tradeId {
number
parser
}
product {
name {
en
fr
de
}
_brand {
... on Brand {
name
}
}
}
}
}
@@ -108,9 +117,83 @@ query GetShipments($state: ShipmentState) {
provider
}
}
pageInfo {
hasNextPage
endCursor
}
}
"""
QUERY_SHIPMENTS_CONFIRMED = """
query {
shipments(state: confirmed) {
nodes {
id
number
state
order {
id
number
email
total
completedAt
locale
shipAddress {
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {
name { en fr de }
iso
}
}
billAddress {
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {
name { en fr de }
iso
}
}
}
inventoryUnits {
id
state
variant {
id
sku
mpn
price
tradeId {
number
parser
}
product {
name {
en
fr
de
}
_brand {
... on Brand {
name
}
}
}
}
}
tracking {
code
provider
}
}
}
}
@@ -123,25 +206,65 @@ query GetShipment($id: ID!) {
id
number
state
createdAt
updatedAt
order {
id
number
email
totalPrice {
amount
currency
total
completedAt
locale
shipAddress {
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {
name { en fr de }
iso
}
}
billAddress {
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {
name { en fr de }
iso
}
}
}
inventoryUnits {
nodes {
id
state
variant {
id
state
variant {
id
sku
name
sku
mpn
price
tradeId {
number
parser
}
product {
name {
en
fr
de
}
_brand {
... on Brand {
name
}
}
}
}
}
@@ -154,6 +277,83 @@ query GetShipment($id: ID!) {
}
"""
# ============================================================================
# Paginated Queries (for historical import)
# ============================================================================
# Note: Using string formatting for state since Letzshop has issues with enum variables
# Note: tracking field removed - causes 'demodulize' server error on some shipments
QUERY_SHIPMENTS_PAGINATED_TEMPLATE = """
query GetShipmentsPaginated($first: Int!, $after: String) {{
shipments(state: {state}, first: $first, after: $after) {{
pageInfo {{
hasNextPage
endCursor
}}
nodes {{
id
number
state
order {{
id
number
email
total
completedAt
locale
shipAddress {{
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {{
iso
}}
}}
billAddress {{
firstName
lastName
company
streetName
streetNumber
city
zipCode
phone
country {{
iso
}}
}}
}}
inventoryUnits {{
id
state
variant {{
id
sku
mpn
price
tradeId {{
number
parser
}}
product {{
name {{
en
fr
de
}}
}}
}}
}}
}}
}}
}}
"""
# ============================================================================
# GraphQL Mutations
# ============================================================================
@@ -327,11 +527,14 @@ class LetzshopClient:
f"Invalid JSON response: {response.text[:200]}"
) from e
logger.debug(f"GraphQL response: {data}")
# Check for GraphQL errors
if "errors" in data and data["errors"]:
error_messages = [
err.get("message", "Unknown error") for err in data["errors"]
]
logger.warning(f"GraphQL errors received: {data['errors']}")
raise LetzshopAPIError(
f"GraphQL errors: {'; '.join(error_messages)}",
response_data=data,
@@ -372,24 +575,31 @@ class LetzshopClient:
def get_shipments(
self,
state: str | None = None,
state: str = "unconfirmed",
) -> list[dict[str, Any]]:
"""
Get shipments from Letzshop.
Args:
state: Optional state filter (e.g., "unconfirmed", "confirmed").
state: State filter ("unconfirmed" or "confirmed").
Returns:
List of shipment data dictionaries.
"""
variables = {}
if state:
variables["state"] = state
# Use pre-built queries with inline state values
# (Letzshop's GraphQL has issues with enum variables)
if state == "confirmed":
query = QUERY_SHIPMENTS_CONFIRMED
else:
query = QUERY_SHIPMENTS_UNCONFIRMED
data = self._execute(QUERY_SHIPMENTS, variables)
logger.debug(f"Fetching shipments with state: {state}")
data = self._execute(query)
logger.debug(f"Shipments response data keys: {data.keys() if data else 'None'}")
shipments_data = data.get("shipments", {})
return shipments_data.get("nodes", [])
nodes = shipments_data.get("nodes", [])
logger.info(f"Got {len(nodes)} {state} shipments from Letzshop API")
return nodes
def get_unconfirmed_shipments(self) -> list[dict[str, Any]]:
"""Get all unconfirmed shipments."""
@@ -408,6 +618,70 @@ class LetzshopClient:
data = self._execute(QUERY_SHIPMENT_BY_ID, {"id": shipment_id})
return data.get("node")
def get_all_shipments_paginated(
self,
state: str = "confirmed",
page_size: int = 50,
max_pages: int | None = None,
progress_callback: Callable[[int, int], None] | None = None,
) -> list[dict[str, Any]]:
"""
Fetch all shipments with pagination support.
Args:
state: State filter ("unconfirmed" or "confirmed").
page_size: Number of shipments per page (default 50).
max_pages: Maximum number of pages to fetch (None = all).
progress_callback: Optional callback(page, total_fetched) for progress updates.
Returns:
List of all shipment data dictionaries.
"""
query = QUERY_SHIPMENTS_PAGINATED_TEMPLATE.format(state=state)
all_shipments = []
cursor = None
page = 0
while True:
page += 1
variables = {"first": page_size}
if cursor:
variables["after"] = cursor
logger.info(f"Fetching {state} shipments page {page} (cursor: {cursor})")
try:
data = self._execute(query, variables)
except LetzshopAPIError as e:
# Log error but return what we have so far
logger.error(f"Error fetching page {page}: {e}")
break
shipments_data = data.get("shipments", {})
nodes = shipments_data.get("nodes", [])
page_info = shipments_data.get("pageInfo", {})
all_shipments.extend(nodes)
if progress_callback:
progress_callback(page, len(all_shipments))
logger.info(f"Page {page}: fetched {len(nodes)} shipments, total: {len(all_shipments)}")
# Check if there are more pages
if not page_info.get("hasNextPage"):
logger.info(f"Reached last page. Total shipments: {len(all_shipments)}")
break
cursor = page_info.get("endCursor")
# Check max pages limit
if max_pages and page >= max_pages:
logger.info(f"Reached max pages limit ({max_pages}). Total shipments: {len(all_shipments)}")
break
return all_shipments
# ========================================================================
# Fulfillment Mutations
# ========================================================================

View File

@@ -20,6 +20,7 @@ from models.database.letzshop import (
VendorLetzshopCredentials,
)
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.product import Product
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)
@@ -197,6 +198,31 @@ class LetzshopOrderService:
return orders, total
def get_order_stats(self, vendor_id: int) -> dict[str, int]:
"""
Get order counts by sync_status for a vendor.
Returns:
Dict with counts for each status: pending, confirmed, rejected, shipped
"""
status_counts = (
self.db.query(
LetzshopOrder.sync_status,
func.count(LetzshopOrder.id).label("count"),
)
.filter(LetzshopOrder.vendor_id == vendor_id)
.group_by(LetzshopOrder.sync_status)
.all()
)
# Convert to dict with default 0 for missing statuses
stats = {"pending": 0, "confirmed": 0, "rejected": 0, "shipped": 0}
for status, count in status_counts:
if status in stats:
stats[status] = count
return stats
def create_order(
self,
vendor_id: int,
@@ -205,21 +231,94 @@ class LetzshopOrderService:
"""Create a new Letzshop order from shipment data."""
order_data = shipment_data.get("order", {})
# Handle total - can be a string like "99.99 EUR" or just a number
total = order_data.get("total", "")
total_amount = str(total) if total else ""
# Default currency to EUR (Letzshop is Luxembourg-based)
currency = "EUR"
# Extract customer name from shipping address
ship_address = order_data.get("shipAddress", {}) or {}
first_name = ship_address.get("firstName", "") or ""
last_name = ship_address.get("lastName", "") or ""
customer_name = f"{first_name} {last_name}".strip() or None
# Extract customer locale (language preference for invoicing)
customer_locale = order_data.get("locale")
# Extract country codes
ship_country = ship_address.get("country", {}) or {}
shipping_country_iso = ship_country.get("iso")
bill_address = order_data.get("billAddress", {}) or {}
bill_country = bill_address.get("country", {}) or {}
billing_country_iso = bill_country.get("iso")
# inventoryUnits is a direct array, not wrapped in nodes
inventory_units_data = shipment_data.get("inventoryUnits", [])
if isinstance(inventory_units_data, dict):
# Handle legacy format with nodes wrapper
inventory_units_data = inventory_units_data.get("nodes", [])
# Extract enriched inventory unit data with product details
enriched_units = []
for unit in inventory_units_data:
variant = unit.get("variant", {}) or {}
product = variant.get("product", {}) or {}
trade_id = variant.get("tradeId") or {}
product_name = product.get("name", {}) or {}
enriched_unit = {
"id": unit.get("id"),
"state": unit.get("state"),
# Product identifiers
"ean": trade_id.get("number"),
"ean_type": trade_id.get("parser"),
"sku": variant.get("sku"),
"mpn": variant.get("mpn"),
# Product info
"product_name": (
product_name.get("en")
or product_name.get("fr")
or product_name.get("de")
),
"product_name_translations": product_name,
# Pricing
"price": variant.get("price"),
"variant_id": variant.get("id"),
}
enriched_units.append(enriched_unit)
# Map Letzshop state to sync_status
# Letzshop shipment states (from docs):
# - unconfirmed: needs to be confirmed/rejected
# - confirmed: at least one product confirmed
# - declined: all products rejected
# Note: "shipped" is not a state - tracking is set separately via tracking field
letzshop_state = shipment_data.get("state", "unconfirmed")
state_mapping = {
"unconfirmed": "pending",
"confirmed": "confirmed",
"declined": "rejected",
}
sync_status = state_mapping.get(letzshop_state, "confirmed")
order = LetzshopOrder(
vendor_id=vendor_id,
letzshop_order_id=order_data.get("id", ""),
letzshop_shipment_id=shipment_data["id"],
letzshop_order_number=order_data.get("number"),
letzshop_state=shipment_data.get("state"),
letzshop_state=letzshop_state,
customer_email=order_data.get("email"),
total_amount=str(order_data.get("totalPrice", {}).get("amount", "")),
currency=order_data.get("totalPrice", {}).get("currency", "EUR"),
customer_name=customer_name,
customer_locale=customer_locale,
shipping_country_iso=shipping_country_iso,
billing_country_iso=billing_country_iso,
total_amount=total_amount,
currency=currency,
raw_order_data=shipment_data,
inventory_units=[
{"id": u["id"], "state": u["state"]}
for u in shipment_data.get("inventoryUnits", {}).get("nodes", [])
],
sync_status="pending",
inventory_units=enriched_units,
sync_status=sync_status,
)
self.db.add(order)
return order
@@ -230,8 +329,66 @@ class LetzshopOrderService:
shipment_data: dict[str, Any],
) -> LetzshopOrder:
"""Update an existing order from shipment data."""
order.letzshop_state = shipment_data.get("state")
order_data = shipment_data.get("order", {})
# Update letzshop_state and sync_status
# Letzshop states: unconfirmed, confirmed, declined
letzshop_state = shipment_data.get("state", "unconfirmed")
state_mapping = {
"unconfirmed": "pending",
"confirmed": "confirmed",
"declined": "rejected",
}
order.letzshop_state = letzshop_state
order.sync_status = state_mapping.get(letzshop_state, "confirmed")
order.raw_order_data = shipment_data
# Update locale if not already set
if not order.customer_locale and order_data.get("locale"):
order.customer_locale = order_data.get("locale")
# Update country codes if not already set
if not order.shipping_country_iso:
ship_address = order_data.get("shipAddress", {}) or {}
ship_country = ship_address.get("country", {}) or {}
order.shipping_country_iso = ship_country.get("iso")
if not order.billing_country_iso:
bill_address = order_data.get("billAddress", {}) or {}
bill_country = bill_address.get("country", {}) or {}
order.billing_country_iso = bill_country.get("iso")
# Update enriched inventory units
inventory_units_data = shipment_data.get("inventoryUnits", [])
if isinstance(inventory_units_data, dict):
inventory_units_data = inventory_units_data.get("nodes", [])
enriched_units = []
for unit in inventory_units_data:
variant = unit.get("variant", {}) or {}
product = variant.get("product", {}) or {}
trade_id = variant.get("tradeId") or {}
product_name = product.get("name", {}) or {}
enriched_unit = {
"id": unit.get("id"),
"state": unit.get("state"),
"ean": trade_id.get("number"),
"ean_type": trade_id.get("parser"),
"sku": variant.get("sku"),
"mpn": variant.get("mpn"),
"product_name": (
product_name.get("en")
or product_name.get("fr")
or product_name.get("de")
),
"product_name_translations": product_name,
"price": variant.get("price"),
"variant_id": variant.get("id"),
}
enriched_units.append(enriched_unit)
order.inventory_units = enriched_units
return order
def mark_order_confirmed(self, order: LetzshopOrder) -> LetzshopOrder:
@@ -415,3 +572,222 @@ class LetzshopOrderService:
jobs = jobs[skip : skip + limit]
return jobs, total
# =========================================================================
# Historical Import Operations
# =========================================================================
def import_historical_shipments(
self,
vendor_id: int,
shipments: list[dict[str, Any]],
match_products: bool = True,
) -> dict[str, Any]:
"""
Import historical shipments into the database.
Args:
vendor_id: Vendor ID to import for.
shipments: List of shipment data from Letzshop API.
match_products: Whether to match EAN to local products.
Returns:
Dict with import statistics:
- total: Total shipments processed
- imported: New orders created
- updated: Existing orders updated
- skipped: Already up-to-date orders
- products_matched: Products matched by EAN
- products_not_found: Products not found in local catalog
"""
stats = {
"total": len(shipments),
"imported": 0,
"updated": 0,
"skipped": 0,
"products_matched": 0,
"products_not_found": 0,
"eans_processed": set(),
"eans_matched": set(),
"eans_not_found": set(),
}
for shipment in shipments:
shipment_id = shipment.get("id")
if not shipment_id:
continue
# Check if order already exists
existing_order = self.get_order_by_shipment_id(vendor_id, shipment_id)
if existing_order:
# Check if we need to update (e.g., state changed)
if existing_order.letzshop_state != shipment.get("state"):
self.update_order_from_shipment(existing_order, shipment)
stats["updated"] += 1
else:
stats["skipped"] += 1
else:
# Create new order
self.create_order(vendor_id, shipment)
stats["imported"] += 1
# Process EANs for matching
if match_products:
inventory_units = shipment.get("inventoryUnits", [])
for unit in inventory_units:
variant = unit.get("variant", {}) or {}
trade_id = variant.get("tradeId") or {}
ean = trade_id.get("number")
if ean:
stats["eans_processed"].add(ean)
# Match EANs to local products
if match_products and stats["eans_processed"]:
matched, not_found = self._match_eans_to_products(
vendor_id, list(stats["eans_processed"])
)
stats["eans_matched"] = matched
stats["eans_not_found"] = not_found
stats["products_matched"] = len(matched)
stats["products_not_found"] = len(not_found)
# Convert sets to lists for JSON serialization
stats["eans_processed"] = list(stats["eans_processed"])
stats["eans_matched"] = list(stats["eans_matched"])
stats["eans_not_found"] = list(stats["eans_not_found"])
return stats
def _match_eans_to_products(
self,
vendor_id: int,
eans: list[str],
) -> tuple[set[str], set[str]]:
"""
Match EAN codes to local products.
Args:
vendor_id: Vendor ID to search products for.
eans: List of EAN codes to match.
Returns:
Tuple of (matched_eans, not_found_eans).
"""
if not eans:
return set(), set()
# Query products by GTIN for this vendor
products = (
self.db.query(Product)
.filter(
Product.vendor_id == vendor_id,
Product.gtin.in_(eans),
)
.all()
)
matched_eans = {p.gtin for p in products if p.gtin}
not_found_eans = set(eans) - matched_eans
logger.info(
f"EAN matching: {len(matched_eans)} matched, {len(not_found_eans)} not found"
)
return matched_eans, not_found_eans
def get_products_by_eans(
self,
vendor_id: int,
eans: list[str],
) -> dict[str, Product]:
"""
Get products by their EAN codes.
Args:
vendor_id: Vendor ID to search products for.
eans: List of EAN codes to search.
Returns:
Dict mapping EAN to Product.
"""
if not eans:
return {}
products = (
self.db.query(Product)
.filter(
Product.vendor_id == vendor_id,
Product.gtin.in_(eans),
)
.all()
)
return {p.gtin: p for p in products if p.gtin}
def get_historical_import_summary(
self,
vendor_id: int,
) -> dict[str, Any]:
"""
Get summary of historical order data for a vendor.
Returns:
Dict with summary statistics.
"""
# Count orders by state
order_counts = (
self.db.query(
LetzshopOrder.letzshop_state,
func.count(LetzshopOrder.id).label("count"),
)
.filter(LetzshopOrder.vendor_id == vendor_id)
.group_by(LetzshopOrder.letzshop_state)
.all()
)
# Count orders by locale
locale_counts = (
self.db.query(
LetzshopOrder.customer_locale,
func.count(LetzshopOrder.id).label("count"),
)
.filter(LetzshopOrder.vendor_id == vendor_id)
.group_by(LetzshopOrder.customer_locale)
.all()
)
# Count orders by country
country_counts = (
self.db.query(
LetzshopOrder.shipping_country_iso,
func.count(LetzshopOrder.id).label("count"),
)
.filter(LetzshopOrder.vendor_id == vendor_id)
.group_by(LetzshopOrder.shipping_country_iso)
.all()
)
# Total revenue
total_orders = (
self.db.query(func.count(LetzshopOrder.id))
.filter(LetzshopOrder.vendor_id == vendor_id)
.scalar()
or 0
)
# Unique customers
unique_customers = (
self.db.query(func.count(func.distinct(LetzshopOrder.customer_email)))
.filter(LetzshopOrder.vendor_id == vendor_id)
.scalar()
or 0
)
return {
"total_orders": total_orders,
"unique_customers": unique_customers,
"orders_by_state": {state: count for state, count in order_counts},
"orders_by_locale": {locale or "unknown": count for locale, count in locale_counts},
"orders_by_country": {country or "unknown": count for country, count in country_counts},
}

View File

@@ -1,21 +1,57 @@
{# app/templates/admin/partials/letzshop-orders-tab.html #}
{# Orders tab for admin Letzshop management #}
<!-- Header with Import Button -->
<!-- Header with Import Buttons -->
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Orders</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Manage Letzshop orders for this vendor</p>
</div>
<button
@click="importOrders()"
:disabled="!letzshopStatus.is_configured || importingOrders"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!importingOrders" x-html="$icon('download', 'w-4 h-4 mr-2')"></span>
<span x-show="importingOrders" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="importingOrders ? 'Importing...' : 'Import Orders'"></span>
</button>
<div class="flex gap-2">
<button
@click="importHistoricalOrders()"
:disabled="!letzshopStatus.is_configured || importingHistorical"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
title="Import all historical confirmed orders"
>
<span x-show="!importingHistorical" x-html="$icon('archive', 'w-4 h-4 mr-2')"></span>
<span x-show="importingHistorical" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="importingHistorical ? 'Importing...' : 'Import History'"></span>
</button>
<button
@click="importOrders()"
:disabled="!letzshopStatus.is_configured || importingOrders"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!importingOrders" x-html="$icon('download', 'w-4 h-4 mr-2')"></span>
<span x-show="importingOrders" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="importingOrders ? 'Importing...' : 'Import New'"></span>
</button>
</div>
</div>
<!-- Historical Import Result -->
<div x-show="historicalImportResult" x-transition class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex items-start justify-between">
<div class="flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 text-blue-500 mr-3 mt-0.5')"></span>
<div>
<h4 class="font-medium text-blue-800 dark:text-blue-200">Historical Import Complete</h4>
<div class="text-sm text-blue-700 dark:text-blue-300 mt-1">
<span x-text="historicalImportResult?.imported + ' imported'"></span> ·
<span x-text="historicalImportResult?.updated + ' updated'"></span> ·
<span x-text="historicalImportResult?.skipped + ' skipped'"></span>
</div>
<div x-show="historicalImportResult?.products_matched > 0 || historicalImportResult?.products_not_found > 0" class="text-sm text-blue-600 dark:text-blue-400 mt-1">
<span x-text="historicalImportResult?.products_matched + ' products matched by EAN'"></span> ·
<span x-text="historicalImportResult?.products_not_found + ' not found'"></span>
</div>
</div>
</div>
<button @click="historicalImportResult = null" class="text-blue-500 hover:text-blue-700">
<span x-html="$icon('x', 'w-4 h-4')"></span>
</button>
</div>
</div>
<!-- Status Cards -->