feat: add image preview functionality and improve product detail layout in ProductDetailPage
This commit is contained in:
parent
a851fc4a50
commit
afab715b56
|
|
@ -1,4 +1,6 @@
|
|||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { Modal } from '../../../components/ui/Modal';
|
||||
import { ArrowRight, Edit, Package, Tag, Image, Calendar, FileText, Eye, DollarSign, Hash, Layers, Settings } from 'lucide-react';
|
||||
import { Button } from '../../../components/ui/Button';
|
||||
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
|
||||
|
|
@ -20,6 +22,8 @@ const ProductDetailPage = () => {
|
|||
return new Intl.NumberFormat('fa-IR').format(price) + ' تومان';
|
||||
};
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('fa-IR').format(num);
|
||||
};
|
||||
|
|
@ -30,486 +34,493 @@ const ProductDetailPage = () => {
|
|||
const variants = (product.variants && product.variants.length > 0) ? product.variants : ((product as any).product_variants || []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate('/products')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
بازگشت
|
||||
</Button>
|
||||
<div>
|
||||
<PageTitle>جزئیات محصول</PageTitle>
|
||||
<p className="text-gray-600 dark:text-gray-400">{product.name}</p>
|
||||
<>
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate('/products')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
بازگشت
|
||||
</Button>
|
||||
<div>
|
||||
<PageTitle>جزئیات محصول</PageTitle>
|
||||
<p className="text-gray-600 dark:text-gray-400">{product.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/products/${id}/edit`)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
ویرایش
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/products/${id}/edit`)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
ویرایش
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* اطلاعات اصلی */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* اطلاعات پایه */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-6">
|
||||
<Package className="h-5 w-5" />
|
||||
اطلاعات محصول
|
||||
</SectionTitle>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* اطلاعات اصلی */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* اطلاعات پایه */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-6">
|
||||
<Package className="h-5 w-5" />
|
||||
اطلاعات محصول
|
||||
</SectionTitle>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
نام محصول
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{product.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.sku && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
کد محصول (SKU)
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-mono">
|
||||
{product.sku}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product.description && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
توضیحات
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.design_style && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
سبک طراحی
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{product.design_style}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
نوع محصول
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{PRODUCT_TYPE_LABELS[product.type as keyof typeof PRODUCT_TYPE_LABELS] || 'نامشخص'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
وضعیت
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${product.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||
}`}>
|
||||
{product.enabled ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.price && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
قیمت
|
||||
نام محصول
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{formatPrice(product.price)}
|
||||
{product.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.sku && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
کد محصول (SKU)
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-mono">
|
||||
{product.sku}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product.description && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
توضیحات
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ویژگیهای محصول */}
|
||||
{product.attributes && Object.keys(product.attributes).length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Settings className="h-5 w-5" />
|
||||
ویژگیهای محصول
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(product.attributes).map(([key, value]) => (
|
||||
<div key={key} className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{key}
|
||||
</span>
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
{product.design_style && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
سبک طراحی
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{product.design_style}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* تصاویر محصول */}
|
||||
{images.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Image className="h-5 w-5" />
|
||||
تصاویر محصول
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{images.map((image: any, index: number) => (
|
||||
<div key={image.id || index} className="relative group">
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.alt || `تصویر ${index + 1}`}
|
||||
className="w-full h-32 object-cover rounded-lg border border-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center">
|
||||
<Eye className="h-6 w-6 text-white" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
نوع محصول
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
{PRODUCT_TYPE_LABELS[product.type as keyof typeof PRODUCT_TYPE_LABELS] || 'نامشخص'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* نسخههای محصول */}
|
||||
{(variants.length > 0) && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Layers className="h-5 w-5" />
|
||||
نسخههای محصول
|
||||
</SectionTitle>
|
||||
<div className="space-y-6">
|
||||
{variants.map((variant, index) => (
|
||||
<div key={variant.id || index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100">
|
||||
نسخه {index + 1}
|
||||
</h4>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${variant.enabled
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
وضعیت
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${product.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||
}`}>
|
||||
{variant.enabled ? 'فعال' : 'غیرفعال'}
|
||||
{product.enabled ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد کارمزد</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.fee_percentage)}%
|
||||
</span>
|
||||
{product.price && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
قیمت
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{formatPrice(product.price)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد سود</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.profit_percentage)}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ویژگیهای محصول */}
|
||||
{product.attributes && Object.keys(product.attributes).length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Settings className="h-5 w-5" />
|
||||
ویژگیهای محصول
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(product.attributes).map(([key, value]) => (
|
||||
<div key={key} className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{key}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">وزن</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.weight)} گرم
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">موجودی</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.stock_number)}
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">حد موجودی</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.stock_limit)}
|
||||
</span>
|
||||
{/* تصاویر محصول */}
|
||||
{images.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Image className="h-5 w-5" />
|
||||
تصاویر محصول
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{images.map((image: any, index: number) => (
|
||||
<div key={image.id || index} className="relative group">
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.alt || `تصویر ${index + 1}`}
|
||||
className="w-full h-32 object-cover rounded-lg border border-gray-200 dark:border-gray-600 cursor-pointer" onClick={() => setPreviewImage(image.url)}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center">
|
||||
<Eye className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">مدیریت موجودی</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${variant.stock_managed
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* نسخههای محصول */}
|
||||
{(variants.length > 0) && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Layers className="h-5 w-5" />
|
||||
نسخههای محصول
|
||||
</SectionTitle>
|
||||
<div className="space-y-6">
|
||||
{variants.map((variant, index) => (
|
||||
<div key={variant.id || index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100">
|
||||
نسخه {index + 1}
|
||||
</h4>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${variant.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||
}`}>
|
||||
{variant.stock_managed ? 'فعال' : 'غیرفعال'}
|
||||
{variant.enabled ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</div>
|
||||
{variant.product_option_id && (
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">گزینه محصول</span>
|
||||
<span className="font-medium text-blue-900 dark:text-blue-100">
|
||||
شناسه: {variant.product_option_id}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد کارمزد</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.fee_percentage)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد سود</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.profit_percentage)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">وزن</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.weight)} گرم
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">موجودی</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.stock_number)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">حد موجودی</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.stock_limit)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">مدیریت موجودی</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${variant.stock_managed
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||
}`}>
|
||||
{variant.stock_managed ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</div>
|
||||
{variant.product_option_id && (
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">گزینه محصول</span>
|
||||
<span className="font-medium text-blue-900 dark:text-blue-100">
|
||||
شناسه: {variant.product_option_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ویژگیهای نسخه */}
|
||||
{variant.attributes && Object.keys(variant.attributes).length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
ویژگیهای نسخه
|
||||
</h5>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(variant.attributes).map(([key, value]) => (
|
||||
<div key={key} className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{key}:</span>
|
||||
<span className="ml-1 text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* متاداده نسخه */}
|
||||
{variant.meta && Object.keys(variant.meta).length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
اطلاعات تکمیلی
|
||||
</h5>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(variant.meta).map(([key, value]) => (
|
||||
<div key={key} className="p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{key}:</span>
|
||||
<span className="ml-1 text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* تصاویر نسخه */}
|
||||
{(variant.file_ids && variant.file_ids.length > 0 || (variant as any).files?.length > 0) && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
تصاویر نسخه
|
||||
</h5>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{((variant.file_ids && variant.file_ids.length > 0 ? variant.file_ids : (variant as any).files) as any[]).map((image: any, imgIndex: number) => (
|
||||
<img
|
||||
key={image.id || imgIndex}
|
||||
src={image.url}
|
||||
alt={image.alt || `تصویر نسخه ${imgIndex + 1}`}
|
||||
className="w-full h-16 object-cover rounded border border-gray-200 dark:border-gray-600 cursor-pointer" onClick={() => setPreviewImage(image.url)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ویژگیهای نسخه */}
|
||||
{variant.attributes && Object.keys(variant.attributes).length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
ویژگیهای نسخه
|
||||
</h5>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(variant.attributes).map(([key, value]) => (
|
||||
<div key={key} className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{key}:</span>
|
||||
<span className="ml-1 text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* متاداده نسخه */}
|
||||
{variant.meta && Object.keys(variant.meta).length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
اطلاعات تکمیلی
|
||||
</h5>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(variant.meta).map(([key, value]) => (
|
||||
<div key={key} className="p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">{key}:</span>
|
||||
<span className="ml-1 text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* تصاویر نسخه */}
|
||||
{variant.file_ids && variant.file_ids.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
تصاویر نسخه
|
||||
</h5>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{variant.file_ids.map((image, imgIndex) => (
|
||||
<img
|
||||
key={image.id || imgIndex}
|
||||
src={image.url}
|
||||
alt={image.alt || `تصویر نسخه ${imgIndex + 1}`}
|
||||
className="w-full h-16 object-cover rounded border border-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* اطلاعات جانبی */}
|
||||
<div className="space-y-6">
|
||||
{/* آمار */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<DollarSign className="h-5 w-5" />
|
||||
آمار محصول
|
||||
</SectionTitle>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hash className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
شناسه محصول
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{product.id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد فروش
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(product.total_sold || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{variants.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد نسخهها
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variants.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{images.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد تصاویر
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(images.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد دستهبندیها
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(product.categories.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* دستهبندیها */}
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
{/* اطلاعات جانبی */}
|
||||
<div className="space-y-6">
|
||||
{/* آمار */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Tag className="h-5 w-5" />
|
||||
دستهبندیها
|
||||
<DollarSign className="h-5 w-5" />
|
||||
آمار محصول
|
||||
</SectionTitle>
|
||||
<div className="space-y-2">
|
||||
{product.categories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="flex items-center gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
>
|
||||
<Tag className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-blue-900 dark:text-blue-100 font-medium">
|
||||
{category.name}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hash className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
شناسه محصول
|
||||
</span>
|
||||
{category.description && (
|
||||
<span className="text-xs text-blue-700 dark:text-blue-300">
|
||||
- {category.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{product.id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* گزینه محصول */}
|
||||
{product.product_option && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Settings className="h-5 w-5" />
|
||||
گزینه محصول
|
||||
</SectionTitle>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{product.product_option.title}
|
||||
</p>
|
||||
{product.product_option.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{product.product_option.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد فروش
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(product.total_sold || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{variants.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد نسخهها
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variants.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{images.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد تصاویر
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(images.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد دستهبندیها
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(product.categories.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* اطلاعات زمانی */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Calendar className="h-5 w-5" />
|
||||
اطلاعات زمانی
|
||||
</SectionTitle>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Calendar className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تاریخ ایجاد
|
||||
</span>
|
||||
{/* دستهبندیها */}
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Tag className="h-5 w-5" />
|
||||
دستهبندیها
|
||||
</SectionTitle>
|
||||
<div className="space-y-2">
|
||||
{product.categories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="flex items-center gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
>
|
||||
<Tag className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-blue-900 dark:text-blue-100 font-medium">
|
||||
{category.name}
|
||||
</span>
|
||||
{category.description && (
|
||||
<span className="text-xs text-blue-700 dark:text-blue-300">
|
||||
- {category.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{new Date(product.created_at).toLocaleDateString('fa-IR')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileText className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
آخرین بهروزرسانی
|
||||
</span>
|
||||
{/* گزینه محصول */}
|
||||
{product.product_option && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Settings className="h-5 w-5" />
|
||||
گزینه محصول
|
||||
</SectionTitle>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{product.product_option.title}
|
||||
</p>
|
||||
{product.product_option.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{product.product_option.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* اطلاعات زمانی */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Calendar className="h-5 w-5" />
|
||||
اطلاعات زمانی
|
||||
</SectionTitle>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Calendar className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تاریخ ایجاد
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{new Date(product.created_at).toLocaleDateString('fa-IR')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileText className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
آخرین بهروزرسانی
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{new Date(product.updated_at).toLocaleDateString('fa-IR')}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{new Date(product.updated_at).toLocaleDateString('fa-IR')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</PageContainer>
|
||||
{previewImage && (
|
||||
<Modal isOpen={true} onClose={() => setPreviewImage(null)} title="پیشنمایش تصویر" size="xl">
|
||||
<img src={previewImage} alt="تصویر" className="w-full h-auto object-contain" />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ const ProductFormPage = () => {
|
|||
console.log('✅ Successfully extracted category IDs:', categoryIds);
|
||||
|
||||
// تبدیل variants از ProductVariant به ProductVariantFormData - handle both variants and product_variants
|
||||
const variants = product.variants || product.product_variants || [];
|
||||
const formVariants = variants.map(variant => ({
|
||||
const variants = product.variants && product.variants.length > 0 ? product.variants : ((product as any).product_variants || []);
|
||||
const formVariants = variants.map((variant: any) => ({
|
||||
id: variant.id,
|
||||
enabled: variant.enabled,
|
||||
fee_percentage: variant.fee_percentage,
|
||||
|
|
@ -113,7 +113,7 @@ const ProductFormPage = () => {
|
|||
product_option_id: variant.product_option_id || undefined,
|
||||
attributes: variant.attributes || {},
|
||||
meta: variant.meta || {},
|
||||
file_ids: variant.file_ids || []
|
||||
file_ids: (variant.file_ids && variant.file_ids.length > 0 ? variant.file_ids : (variant as any).files || [])
|
||||
}));
|
||||
|
||||
console.log('✅ Successfully processed variants:', formVariants.length);
|
||||
|
|
@ -128,10 +128,12 @@ const ProductFormPage = () => {
|
|||
category_ids: categoryIds,
|
||||
product_option_id: product.product_option_id || undefined,
|
||||
attributes: product.attributes || {},
|
||||
file_ids: product.file_ids || [],
|
||||
file_ids: (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []),
|
||||
variants: formVariants
|
||||
});
|
||||
setUploadedImages(product.file_ids || []);
|
||||
const initialImages = (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []);
|
||||
setUploadedImages(initialImages);
|
||||
setValue('file_ids', initialImages, { shouldValidate: true, shouldDirty: false });
|
||||
setAttributes(product.attributes || {});
|
||||
}
|
||||
}, [isEdit, product, reset]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue