feat: add image preview functionality and improve product detail layout in ProductDetailPage

This commit is contained in:
hossein taromi 2025-07-30 17:18:22 +03:30
parent a851fc4a50
commit afab715b56
2 changed files with 435 additions and 422 deletions

View File

@ -1,4 +1,6 @@
import { useParams, useNavigate } from 'react-router-dom'; 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 { ArrowRight, Edit, Package, Tag, Image, Calendar, FileText, Eye, DollarSign, Hash, Layers, Settings } from 'lucide-react';
import { Button } from '../../../components/ui/Button'; import { Button } from '../../../components/ui/Button';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner'; import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
@ -20,6 +22,8 @@ const ProductDetailPage = () => {
return new Intl.NumberFormat('fa-IR').format(price) + ' تومان'; return new Intl.NumberFormat('fa-IR').format(price) + ' تومان';
}; };
const [previewImage, setPreviewImage] = useState<string | null>(null);
const formatNumber = (num: number) => { const formatNumber = (num: number) => {
return new Intl.NumberFormat('fa-IR').format(num); 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 || []); const variants = (product.variants && product.variants.length > 0) ? product.variants : ((product as any).product_variants || []);
return ( return (
<PageContainer> <>
<div className="flex items-center justify-between mb-6"> <PageContainer>
<div className="flex items-center gap-3"> <div className="flex items-center justify-between mb-6">
<Button <div className="flex items-center gap-3">
variant="secondary" <Button
onClick={() => navigate('/products')} variant="secondary"
className="flex items-center gap-2" onClick={() => navigate('/products')}
> className="flex items-center gap-2"
<ArrowRight className="h-4 w-4" /> >
بازگشت <ArrowRight className="h-4 w-4" />
</Button> بازگشت
<div> </Button>
<PageTitle>جزئیات محصول</PageTitle> <div>
<p className="text-gray-600 dark:text-gray-400">{product.name}</p> <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> </div>
<div className="flex gap-3"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Button {/* اطلاعات اصلی */}
variant="secondary" <div className="lg:col-span-2 space-y-6">
onClick={() => navigate(`/products/${id}/edit`)} {/* اطلاعات پایه */}
className="flex items-center gap-2" <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">
<Edit className="h-4 w-4" /> <Package className="h-5 w-5" />
ویرایش اطلاعات محصول
</Button> </SectionTitle>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="space-y-6">
{/* اطلاعات اصلی */} <div className="grid grid-cols-1 md:grid-cols-2 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> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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> </label>
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-gray-900 dark:text-gray-100 font-medium"> <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> </p>
</div> </div>
</div> </div>
)} )}
</div>
</div>
</div>
{/* ویژگی‌های محصول */} {product.design_style && (
{product.attributes && Object.keys(product.attributes).length > 0 && ( <div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<SectionTitle className="flex items-center gap-2 mb-4"> سبک طراحی
<Settings className="h-5 w-5" /> </label>
ویژگیهای محصول <div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
</SectionTitle> <p className="text-gray-900 dark:text-gray-100">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {product.design_style}
{Object.entries(product.attributes).map(([key, value]) => ( </p>
<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>
</div> </div>
</div> </div>
))} )}
</div>
</div>
)}
{/* تصاویر محصول */} <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{images.length > 0 && ( <div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<SectionTitle className="flex items-center gap-2 mb-4"> نوع محصول
<Image className="h-5 w-5" /> </label>
تصاویر محصول <div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
</SectionTitle> <p className="text-gray-900 dark:text-gray-100">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> {PRODUCT_TYPE_LABELS[product.type as keyof typeof PRODUCT_TYPE_LABELS] || 'نامشخص'}
{images.map((image: any, index: number) => ( </p>
<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> </div>
</div> </div>
))}
</div>
</div>
)}
{/* نسخه‌های محصول */} <div>
{(variants.length > 0) && ( <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<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"> </label>
<Layers className="h-5 w-5" /> <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
</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-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' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
}`}> }`}>
{variant.enabled ? 'فعال' : 'غیرفعال'} {product.enabled ? 'فعال' : 'غیرفعال'}
</span> </span>
</div> </div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4"> {product.price && (
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded"> <div>
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد کارمزد</span> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<span className="font-medium text-gray-900 dark:text-gray-100"> قیمت
{formatNumber(variant.fee_percentage)}% </label>
</span> <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>
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded"> </div>
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد سود</span> )}
<span className="font-medium text-gray-900 dark:text-gray-100"> </div>
{formatNumber(variant.profit_percentage)}% </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>
</div> <span className="text-sm text-gray-900 dark:text-gray-100">
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded"> {typeof value === 'object' ? JSON.stringify(value) : value}
<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> </span>
</div> </div>
</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"> {images.length > 0 && (
<span className="text-xs text-gray-600 dark:text-gray-400 block">حد موجودی</span> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<span className="font-medium text-gray-900 dark:text-gray-100"> <SectionTitle className="flex items-center gap-2 mb-4">
{formatNumber(variant.stock_limit)} <Image className="h-5 w-5" />
</span> تصاویر محصول
</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>
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded"> </div>
<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>
)}
{/* نسخه‌های محصول */}
{(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-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' : 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
}`}> }`}>
{variant.stock_managed ? 'فعال' : 'غیرفعال'} {variant.enabled ? 'فعال' : 'غیرفعال'}
</span> </span>
</div> </div>
{variant.product_option_id && (
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<span className="text-xs text-gray-600 dark:text-gray-400 block">گزینه محصول</span> <div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
<span className="font-medium text-blue-900 dark:text-blue-100"> <span className="text-xs text-gray-600 dark:text-gray-400 block">درصد کارمزد</span>
شناسه: {variant.product_option_id} <span className="font-medium text-gray-900 dark:text-gray-100">
{formatNumber(variant.fee_percentage)}%
</span> </span>
</div> </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> </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> </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"> <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"> <SectionTitle className="flex items-center gap-2 mb-4">
<Tag className="h-5 w-5" /> <DollarSign className="h-5 w-5" />
دستهبندیها آمار محصول
</SectionTitle> </SectionTitle>
<div className="space-y-2"> <div className="space-y-4">
{product.categories.map((category) => ( <div className="flex items-center justify-between">
<div <div className="flex items-center gap-2">
key={category.id} <Hash className="h-4 w-4 text-blue-500" />
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" <span className="text-sm text-gray-600 dark:text-gray-400">
> شناسه محصول
<Tag className="h-4 w-4 text-blue-500" />
<span className="text-blue-900 dark:text-blue-100 font-medium">
{category.name}
</span> </span>
{category.description && (
<span className="text-xs text-blue-700 dark:text-blue-300">
- {category.description}
</span>
)}
</div> </div>
))} <span className="font-semibold text-gray-900 dark:text-gray-100">
</div> {product.id}
</div> </span>
)} </div>
{/* گزینه محصول */} <div className="flex items-center justify-between">
{product.product_option && ( <div className="flex items-center gap-2">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"> <DollarSign className="h-4 w-4 text-green-500" />
<SectionTitle className="flex items-center gap-2 mb-4"> <span className="text-sm text-gray-600 dark:text-gray-400">
<Settings className="h-5 w-5" /> تعداد فروش
گزینه محصول </span>
</SectionTitle> </div>
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <span className="font-semibold text-gray-900 dark:text-gray-100">
<p className="text-gray-900 dark:text-gray-100 font-medium"> {formatNumber(product.total_sold || 0)}
{product.product_option.title} </span>
</p> </div>
{product.product_option.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1"> {variants.length > 0 && (
{product.product_option.description} <div className="flex items-center justify-between">
</p> <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> </div>
)}
{/* اطلاعات زمانی */} {/* دسته‌بندی‌ها */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"> {product.categories && product.categories.length > 0 && (
<SectionTitle className="flex items-center gap-2 mb-4"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<Calendar className="h-5 w-5" /> <SectionTitle className="flex items-center gap-2 mb-4">
اطلاعات زمانی <Tag className="h-5 w-5" />
</SectionTitle> دستهبندیها
<div className="space-y-4"> </SectionTitle>
<div> <div className="space-y-2">
<div className="flex items-center gap-2 mb-1"> {product.categories.map((category) => (
<Calendar className="h-4 w-4 text-green-500" /> <div
<span className="text-sm text-gray-600 dark:text-gray-400"> 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"
</span> >
<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> </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> {/* گزینه محصول */}
<div className="flex items-center gap-2 mb-1"> {product.product_option && (
<FileText className="h-4 w-4 text-orange-500" /> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<span className="text-sm text-gray-600 dark:text-gray-400"> <SectionTitle className="flex items-center gap-2 mb-4">
آخرین بهروزرسانی <Settings className="h-5 w-5" />
</span> گزینه محصول
</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> </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> </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>
)}
</>
); );
}; };

View File

@ -100,8 +100,8 @@ const ProductFormPage = () => {
console.log('✅ Successfully extracted category IDs:', categoryIds); console.log('✅ Successfully extracted category IDs:', categoryIds);
// تبدیل variants از ProductVariant به ProductVariantFormData - handle both variants and product_variants // تبدیل variants از ProductVariant به ProductVariantFormData - handle both variants and product_variants
const variants = product.variants || product.product_variants || []; const variants = product.variants && product.variants.length > 0 ? product.variants : ((product as any).product_variants || []);
const formVariants = variants.map(variant => ({ const formVariants = variants.map((variant: any) => ({
id: variant.id, id: variant.id,
enabled: variant.enabled, enabled: variant.enabled,
fee_percentage: variant.fee_percentage, fee_percentage: variant.fee_percentage,
@ -113,7 +113,7 @@ const ProductFormPage = () => {
product_option_id: variant.product_option_id || undefined, product_option_id: variant.product_option_id || undefined,
attributes: variant.attributes || {}, attributes: variant.attributes || {},
meta: variant.meta || {}, 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); console.log('✅ Successfully processed variants:', formVariants.length);
@ -128,10 +128,12 @@ const ProductFormPage = () => {
category_ids: categoryIds, category_ids: categoryIds,
product_option_id: product.product_option_id || undefined, product_option_id: product.product_option_id || undefined,
attributes: product.attributes || {}, 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 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 || {}); setAttributes(product.attributes || {});
} }
}, [isEdit, product, reset]); }, [isEdit, product, reset]);