fix: بهبود منطق بارگذاری و استخراج نام ویژگی variant

- اصلاح تبدیل variant images از API response
- افزودن منطق استخراج نام ویژگی از variant_attributes
- بهبود fallback برای نمایش صحیح تصاویر variant در حالت ویرایش
- رفع مشکل خالی بودن فیلد نام ویژگی Variant
This commit is contained in:
hosseintaromi 2025-08-01 14:38:27 +03:30
parent 5bf157219e
commit f63dd99e89
1 changed files with 46 additions and 102 deletions

View File

@ -28,9 +28,9 @@ const productSchema = yup.object({
.min(0, 'تعداد فروخته شده نمی‌تواند منفی باشد') .min(0, 'تعداد فروخته شده نمی‌تواند منفی باشد')
.optional(), .optional(),
type: yup.number().oneOf([0, 1, 2, 3]).default(1), type: yup.number().oneOf([0, 1, 2, 3]).default(1),
variant_attribute_name: yup.string().optional(),
category_ids: yup.array().of(yup.number()).default([]), category_ids: yup.array().of(yup.number()).default([]),
product_option_id: yup.number().transform(createOptionalNumberTransform()).nullable(), product_option_id: yup.number().transform(createOptionalNumberTransform()).nullable(),
attributes: yup.object().default({}),
file_ids: yup.array().of(yup.object()).default([]), file_ids: yup.array().of(yup.object()).default([]),
variants: yup.array().default([]), variants: yup.array().default([]),
}); });
@ -41,9 +41,6 @@ const ProductFormPage = () => {
const isEdit = !!id; const isEdit = !!id;
const [uploadedImages, setUploadedImages] = useState<ProductImage[]>([]); const [uploadedImages, setUploadedImages] = useState<ProductImage[]>([]);
const [attributes, setAttributes] = useState<Record<string, any>>({});
const [newAttributeKey, setNewAttributeKey] = useState('');
const [newAttributeValue, setNewAttributeValue] = useState('');
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const { data: product, isLoading: isLoadingProduct } = useProduct(id || '', isEdit); const { data: product, isLoading: isLoadingProduct } = useProduct(id || '', isEdit);
@ -74,9 +71,9 @@ const ProductFormPage = () => {
enabled: true, enabled: true,
total_sold: undefined, total_sold: undefined,
type: 1, type: 1,
variant_attribute_name: '',
category_ids: [], category_ids: [],
product_option_id: undefined, product_option_id: undefined,
attributes: {},
file_ids: [], file_ids: [],
variants: [] variants: []
} }
@ -119,6 +116,19 @@ const ProductFormPage = () => {
console.log('✅ Successfully processed variants:', formVariants.length); console.log('✅ Successfully processed variants:', formVariants.length);
// استخراج نام ویژگی Variant از فیلد variant_attributes
let variantAttributeName = '';
if ((product as any).variant_attributes && Array.isArray((product as any).variant_attributes) && (product as any).variant_attributes.length > 0) {
const firstAttribute = (product as any).variant_attributes[0];
if (firstAttribute && firstAttribute.name) {
variantAttributeName = firstAttribute.name;
}
}
// Fallback to direct field if exists
if (!variantAttributeName && (product as any).variant_attribute_name) {
variantAttributeName = (product as any).variant_attribute_name;
}
reset({ reset({
name: product.name, name: product.name,
description: product.description || '', description: product.description || '',
@ -126,32 +136,33 @@ const ProductFormPage = () => {
enabled: product.enabled, enabled: product.enabled,
total_sold: product.total_sold || 0, total_sold: product.total_sold || 0,
type: 1, type: 1,
variant_attribute_name: variantAttributeName,
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 || {},
file_ids: (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []), file_ids: (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []),
variants: formVariants variants: formVariants
}); });
const initialImages = (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []); const initialImages = (product.file_ids && product.file_ids.length > 0 ? product.file_ids : (product as any).files || []);
setUploadedImages(initialImages); setUploadedImages(initialImages);
setValue('file_ids', initialImages, { shouldValidate: true, shouldDirty: false }); setValue('file_ids', initialImages, { shouldValidate: true, shouldDirty: false });
setAttributes(product.attributes || {});
} }
}, [isEdit, product, reset]); }, [isEdit, product, reset]);
const handleFileUpload = async (file: File) => { const handleFileUpload = async (file: File) => {
try { try {
const result = await uploadFile(file); const result = await uploadFile(file);
const newImage: ProductImage = {
id: result.id,
url: result.url,
alt: file.name,
order: uploadedImages.length
};
const updatedImages = [...uploadedImages, newImage]; setUploadedImages(prev => {
setUploadedImages(updatedImages); const newImage: ProductImage = {
setValue('file_ids', updatedImages, { shouldValidate: true, shouldDirty: true }); id: result.id,
url: result.url,
alt: file.name,
order: prev.length
};
const updated = [...prev, newImage];
setValue('file_ids', updated, { shouldValidate: true, shouldDirty: true });
return updated;
});
return result; return result;
} catch (error) { } catch (error) {
@ -167,25 +178,7 @@ const ProductFormPage = () => {
deleteFile(fileId); deleteFile(fileId);
}; };
const handleAddAttribute = () => {
if (newAttributeKey.trim() && newAttributeValue.trim()) {
const updatedAttributes = {
...attributes,
[newAttributeKey.trim()]: newAttributeValue.trim()
};
setAttributes(updatedAttributes);
setValue('attributes', updatedAttributes, { shouldValidate: true, shouldDirty: true });
setNewAttributeKey('');
setNewAttributeValue('');
}
};
const handleRemoveAttribute = (key: string) => {
const updatedAttributes = { ...attributes };
delete updatedAttributes[key];
setAttributes(updatedAttributes);
setValue('attributes', updatedAttributes, { shouldValidate: true, shouldDirty: true });
};
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
const convertedData = convertPersianNumbersInObject(data); const convertedData = convertPersianNumbersInObject(data);
@ -204,7 +197,8 @@ const ProductFormPage = () => {
enabled: convertedData.enabled, enabled: convertedData.enabled,
total_sold: convertedData.total_sold || 0, total_sold: convertedData.total_sold || 0,
type: 1, type: 1,
attributes: convertPersianNumbersInObject(attributes), variant_attribute_name: convertedData.variant_attribute_name || '',
attributes: {},
category_ids: convertedData.category_ids.length > 0 ? convertedData.category_ids : [], category_ids: convertedData.category_ids.length > 0 ? convertedData.category_ids : [],
product_option_id: convertedData.product_option_id || null, product_option_id: convertedData.product_option_id || null,
file_ids: validImageIds file_ids: validImageIds
@ -225,9 +219,8 @@ const ProductFormPage = () => {
stock_managed: variant.stock_managed, stock_managed: variant.stock_managed,
stock_number: variant.stock_number, stock_number: variant.stock_number,
weight: variant.weight, weight: variant.weight,
product_option_id: variant.product_option_id || null,
file_ids: Array.isArray(variant.file_ids) ? variant.file_ids.map((file: any) => Number(typeof file === 'object' ? file.id : file)).filter((id: number) => !isNaN(id)) : [], file_ids: Array.isArray(variant.file_ids) ? variant.file_ids.map((file: any) => Number(typeof file === 'object' ? file.id : file)).filter((id: number) => !isNaN(id)) : [],
attributes: variant.attributes && Object.keys(variant.attributes).length > 0 ? variant.attributes : {}, attributes: variant.attributes && convertedData.variant_attribute_name && variant.attributes[convertedData.variant_attribute_name] !== undefined ? { [convertedData.variant_attribute_name]: variant.attributes[convertedData.variant_attribute_name] } : {},
meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {} meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {}
})) || []; })) || [];
@ -250,9 +243,8 @@ const ProductFormPage = () => {
stock_managed: variant.stock_managed, stock_managed: variant.stock_managed,
stock_number: variant.stock_number, stock_number: variant.stock_number,
weight: variant.weight, weight: variant.weight,
product_option_id: variant.product_option_id || null,
file_ids: Array.isArray(variant.file_ids) ? variant.file_ids.map((file: any) => Number(typeof file === 'object' ? file.id : file)).filter((id: number) => !isNaN(id)) : [], file_ids: Array.isArray(variant.file_ids) ? variant.file_ids.map((file: any) => Number(typeof file === 'object' ? file.id : file)).filter((id: number) => !isNaN(id)) : [],
attributes: variant.attributes && Object.keys(variant.attributes).length > 0 ? variant.attributes : {}, attributes: variant.attributes && convertedData.variant_attribute_name && variant.attributes[convertedData.variant_attribute_name] !== undefined ? { [convertedData.variant_attribute_name]: variant.attributes[convertedData.variant_attribute_name] } : {},
meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {} meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {}
})) || []; })) || [];
@ -358,6 +350,13 @@ const ProductFormPage = () => {
placeholder="مدرن، کلاسیک، مینیمال..." placeholder="مدرن، کلاسیک، مینیمال..."
/> />
<Input
label="نام ویژگی Variant"
{...register('variant_attribute_name')}
error={errors.variant_attribute_name?.message}
placeholder="مثال: آبکاری، رنگ، سایز..."
/>
<div className="md:col-span-2"> <div className="md:col-span-2">
<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">
توضیحات توضیحات
@ -478,67 +477,11 @@ const ProductFormPage = () => {
variants={watch('variants') || []} variants={watch('variants') || []}
onChange={(variants) => setValue('variants', variants, { shouldValidate: true, shouldDirty: true })} onChange={(variants) => setValue('variants', variants, { shouldValidate: true, shouldDirty: true })}
productOptions={productOptionOptions} productOptions={productOptionOptions}
variantAttributeName={watch('variant_attribute_name')}
/> />
</div> </div>
{/* Custom Attributes */}
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
ویژگیهای سفارشی
</h3>
{/* Add New Attribute */}
<div className="flex gap-3 mb-4">
<input
type="text"
value={newAttributeKey}
onChange={(e) => setNewAttributeKey(e.target.value)}
placeholder="نام ویژگی (مثل: رنگ، سایز)"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
/>
<input
type="text"
value={newAttributeValue}
onChange={(e) => setNewAttributeValue(e.target.value)}
placeholder="مقدار (مثل: قرمز، بزرگ)"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
/>
<Button
type="button"
variant="secondary"
onClick={handleAddAttribute}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
افزودن
</Button>
</div>
{/* Current Attributes */}
{Object.keys(attributes).length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
ویژگیهای فعلی:
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{Object.entries(attributes).map(([key, value]) => (
<div key={key} className="flex items-center justify-between bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded-md">
<span className="text-sm">
<strong>{key}:</strong> {String(value)}
</span>
<button
type="button"
onClick={() => handleRemoveAttribute(key)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
)}
</div>
{/* Preview */} {/* Preview */}
{formValues.name && ( {formValues.name && (
@ -569,6 +512,11 @@ const ProductFormPage = () => {
<strong>استایل:</strong> {formValues.design_style} <strong>استایل:</strong> {formValues.design_style}
</div> </div>
)} )}
{formValues.variant_attribute_name && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>نام ویژگی Variant:</strong> {formValues.variant_attribute_name}
</div>
)}
{formValues.category_ids && formValues.category_ids.length > 0 && ( {formValues.category_ids && formValues.category_ids.length > 0 && (
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
<strong>دستهبندیها:</strong> { <strong>دستهبندیها:</strong> {
@ -582,11 +530,7 @@ const ProductFormPage = () => {
<strong>تعداد Variants:</strong> {formValues.variants.length} نوع <strong>تعداد Variants:</strong> {formValues.variants.length} نوع
</div> </div>
)} )}
{Object.keys(attributes).length > 0 && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>ویژگیها:</strong> {Object.keys(attributes).length} مورد
</div>
)}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{formValues.enabled && ( {formValues.enabled && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800"> <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
@ -634,7 +578,7 @@ const ProductFormPage = () => {
<li> نام محصول باید واضح و جذاب باشد</li> <li> نام محصول باید واضح و جذاب باشد</li>
<li> میتوانید چندین دستهبندی برای محصول انتخاب کنید</li> <li> میتوانید چندین دستهبندی برای محصول انتخاب کنید</li>
<li> گزینه محصول برای محصولات متغیر (با رنگ، سایز و...) استفاده میشود</li> <li> گزینه محصول برای محصولات متغیر (با رنگ، سایز و...) استفاده میشود</li>
<li> ویژگیهای سفارشی برای اطلاعات اضافی محصول مفید هستند</li> <li> نام ویژگی Variant برای تعیین کلید ویژگی در هر variant استفاده میشود</li>
<li> Variants برای انواع مختلف محصول استفاده میشود</li> <li> Variants برای انواع مختلف محصول استفاده میشود</li>
<li> اولین تصویر به عنوان تصویر اصلی محصول استفاده میشود</li> <li> اولین تصویر به عنوان تصویر اصلی محصول استفاده میشود</li>
</ul> </ul>