- Add admin marketplace products page to browse imported products - Add admin vendor products page to manage vendor catalog - Add product detail pages for both marketplace and vendor products - Implement copy-to-vendor API to copy marketplace products to vendor catalogs - Add vendor product service with CRUD operations - Update sidebar navigation with new product management links - Add integration and unit tests for new endpoints and services 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
337 lines
19 KiB
HTML
337 lines
19 KiB
HTML
{# app/templates/admin/vendor-product-detail.html #}
|
|
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
|
{% from 'shared/macros/headers.html' import detail_page_header %}
|
|
{% from 'shared/macros/modals.html' import modal_simple %}
|
|
|
|
{% block title %}Vendor Product Details{% endblock %}
|
|
|
|
{% block alpine_data %}adminVendorProductDetail(){% endblock %}
|
|
|
|
{% block content %}
|
|
{% call detail_page_header("product?.title || 'Product Details'", '/admin/vendor-products', subtitle_show='product') %}
|
|
<span x-text="product?.vendor_name || 'Unknown Vendor'"></span>
|
|
<span class="text-gray-400 mx-2">|</span>
|
|
<span x-text="product?.vendor_code || ''"></span>
|
|
{% endcall %}
|
|
|
|
{{ loading_state('Loading product details...') }}
|
|
|
|
{{ error_state('Error loading product') }}
|
|
|
|
<!-- Product Details -->
|
|
<div x-show="!loading && product">
|
|
<!-- Override Info Banner -->
|
|
<div class="px-4 py-3 mb-6 bg-purple-50 dark:bg-purple-900/20 rounded-lg shadow-md">
|
|
<div class="flex items-start">
|
|
<span x-html="$icon('information-circle', 'w-5 h-5 text-purple-500 mr-3 mt-0.5 flex-shrink-0')"></span>
|
|
<div>
|
|
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Vendor Product Catalog Entry</p>
|
|
<p class="text-xs text-purple-600 dark:text-purple-400 mt-1">
|
|
This is a vendor-specific copy of a marketplace product. Fields marked with
|
|
<span class="inline-flex items-center px-1.5 py-0.5 mx-1 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
|
|
have been customized for this vendor.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions Card -->
|
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Quick Actions
|
|
</h3>
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<a
|
|
:href="'/admin/marketplace-products/' + product?.marketplace_product_id"
|
|
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">
|
|
<span x-html="$icon('database', 'w-4 h-4 mr-2')"></span>
|
|
View Source Product
|
|
</a>
|
|
<button
|
|
@click="openEditModal()"
|
|
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-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600">
|
|
<span x-html="$icon('pencil', 'w-4 h-4 mr-2')"></span>
|
|
Edit Overrides
|
|
</button>
|
|
<button
|
|
@click="toggleActive()"
|
|
:class="product?.is_active
|
|
? 'text-red-700 dark:text-red-300 border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20'
|
|
: 'text-green-700 dark:text-green-300 border-green-300 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-900/20'"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 bg-white dark:bg-gray-700 border rounded-lg">
|
|
<span x-html="$icon(product?.is_active ? 'x-circle' : 'check-circle', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="product?.is_active ? 'Deactivate' : 'Activate'"></span>
|
|
</button>
|
|
<button
|
|
@click="confirmRemove()"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-red-600 dark:text-red-400 transition-colors duration-150 bg-white dark:bg-gray-700 border border-red-300 dark:border-red-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20">
|
|
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
|
|
Remove from Catalog
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Header with Image -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-3">
|
|
<!-- Product Image -->
|
|
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<div class="aspect-square bg-gray-100 dark:bg-gray-700 rounded-lg overflow-hidden">
|
|
<template x-if="product?.image_url">
|
|
<img :src="product?.image_url" :alt="product?.title" class="w-full h-full object-contain" />
|
|
</template>
|
|
<template x-if="!product?.image_url">
|
|
<div class="w-full h-full flex items-center justify-center">
|
|
<span x-html="$icon('photograph', 'w-16 h-16 text-gray-300')"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<!-- Additional Images -->
|
|
<div x-show="product?.additional_images?.length > 0" class="mt-4">
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Additional Images</p>
|
|
<div class="grid grid-cols-4 gap-2">
|
|
<template x-for="(img, index) in (product?.additional_images || [])" :key="index">
|
|
<div class="aspect-square bg-gray-100 dark:bg-gray-700 rounded overflow-hidden">
|
|
<img :src="img" :alt="'Image ' + (index + 1)" class="w-full h-full object-cover" />
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Info -->
|
|
<div class="md:col-span-2 space-y-6">
|
|
<!-- Vendor Info Card -->
|
|
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Vendor Information
|
|
</h3>
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Vendor</p>
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="product?.vendor_name || '-'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Vendor Code</p>
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.vendor_code || '-'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Vendor SKU</p>
|
|
<div class="flex items-center gap-2">
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.vendor_sku || '-'">-</p>
|
|
<span x-show="product?.vendor_sku && product?.vendor_sku !== product?.source_sku" class="px-1.5 py-0.5 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Status</p>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="product?.is_active ? 'text-green-700 bg-green-100 dark:bg-green-900/30 dark:text-green-400' : 'text-red-700 bg-red-100 dark:bg-red-900/30 dark:text-red-400'"
|
|
x-text="product?.is_active ? 'Active' : 'Inactive'">
|
|
</span>
|
|
<span x-show="product?.is_featured" class="px-2 py-1 text-xs font-semibold rounded-full text-yellow-700 bg-yellow-100 dark:bg-yellow-900/30 dark:text-yellow-400">
|
|
Featured
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pricing Card -->
|
|
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Pricing
|
|
</h3>
|
|
<div class="grid gap-4 md:grid-cols-3">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Effective Price</p>
|
|
<div class="flex items-center gap-2">
|
|
<p class="text-lg font-bold text-gray-700 dark:text-gray-200" x-text="formatPrice(product?.effective_price, product?.effective_currency)">-</p>
|
|
<span x-show="product?.price_override" class="px-1.5 py-0.5 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
|
|
</div>
|
|
</div>
|
|
<div x-show="product?.price_override">
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source Price</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 line-through" x-text="formatPrice(product?.source_price, product?.source_currency)">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Availability</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.availability || 'Not specified'">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Information Card -->
|
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Product Information
|
|
</h3>
|
|
<div class="grid gap-4 md:grid-cols-3">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Brand</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.brand || 'No brand'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Product Type</p>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="product?.is_digital ? 'text-blue-700 bg-blue-100 dark:bg-blue-900/30 dark:text-blue-400' : 'text-orange-700 bg-orange-100 dark:bg-orange-900/30 dark:text-orange-400'"
|
|
x-text="product?.is_digital ? 'Digital' : 'Physical'">
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Condition</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.condition || 'Not specified'">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Identifiers Card -->
|
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Product Identifiers
|
|
</h3>
|
|
<div class="grid gap-4 md:grid-cols-4">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Product ID</p>
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.id || '-'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">GTIN/EAN</p>
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.gtin || product?.source_gtin || '-'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Vendor SKU</p>
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.vendor_sku || '-'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source SKU</p>
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.source_sku || '-'">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Source Information Card -->
|
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Source Information
|
|
</h3>
|
|
<a
|
|
:href="'/admin/marketplace-products/' + product?.marketplace_product_id"
|
|
class="flex items-center text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400">
|
|
<span>View Source</span>
|
|
<span x-html="$icon('arrow-right', 'w-4 h-4 ml-1')"></span>
|
|
</a>
|
|
</div>
|
|
<div class="grid gap-4 md:grid-cols-3">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Marketplace</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.source_marketplace || 'Unknown'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source Vendor</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.source_vendor || 'Unknown'">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Marketplace Product ID</p>
|
|
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="product?.marketplace_product_id || '-'">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description Card -->
|
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="product?.title || product?.description">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Product Content
|
|
</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Title</p>
|
|
<span x-show="product?.title_override" class="px-1.5 py-0.5 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
|
|
</div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.title || '-'">-</p>
|
|
</div>
|
|
<div x-show="product?.description">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Description</p>
|
|
<span x-show="product?.description_override" class="px-1.5 py-0.5 text-xs font-medium text-purple-700 bg-purple-100 dark:bg-purple-800 dark:text-purple-300 rounded">Override</span>
|
|
</div>
|
|
<div class="text-sm text-gray-700 dark:text-gray-300 prose prose-sm dark:prose-invert max-w-none" x-html="product?.description || '-'"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category Information -->
|
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="product?.google_product_category || product?.category_path">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Categories
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<div x-show="product?.google_product_category">
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Google Product Category</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.google_product_category">-</p>
|
|
</div>
|
|
<div x-show="product?.category_path">
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Category Path</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="product?.category_path">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Timestamps -->
|
|
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Record Information
|
|
</h3>
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Added to Catalog</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(product?.created_at)">-</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Last Updated</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(product?.updated_at)">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirm Remove Modal -->
|
|
{% call modal_simple('confirmRemoveModal', 'Remove from Catalog', show_var='showRemoveModal', size='sm') %}
|
|
<div class="space-y-4">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Are you sure you want to remove this product from the vendor's catalog?
|
|
</p>
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="product?.title || 'Untitled'"></p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
This will not delete the source product from the marketplace repository.
|
|
</p>
|
|
|
|
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button
|
|
@click="showRemoveModal = false"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="executeRemove()"
|
|
:disabled="removing"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
|
>
|
|
<span x-text="removing ? 'Removing...' : 'Remove'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endcall %}
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="{{ url_for('static', path='admin/js/vendor-product-detail.js') }}"></script>
|
|
{% endblock %}
|