fix: نمایش صحیح تصاویر هنگام ویرایش variant
- اصلاح منطق بارگذاری تصاویر variant در حالت ویرایش - افزودن fallback به فیلد files در صورت خالی بودن file_ids - رفع مشکل نمایش فقط یک تصویر از چندین تصویر آپلود شده
This commit is contained in:
parent
afab715b56
commit
25429f9745
|
|
@ -25,6 +25,9 @@ interface FileUploaderProps {
|
||||||
error?: string;
|
error?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
mode?: 'single' | 'multi';
|
||||||
|
onUploadStart?: () => void;
|
||||||
|
onUploadComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploader: React.FC<FileUploaderProps> = ({
|
export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
|
|
@ -38,6 +41,9 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
error,
|
error,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className = "",
|
className = "",
|
||||||
|
mode = 'multi',
|
||||||
|
onUploadStart,
|
||||||
|
onUploadComplete,
|
||||||
}) => {
|
}) => {
|
||||||
const [files, setFiles] = useState<UploadedFile[]>([]);
|
const [files, setFiles] = useState<UploadedFile[]>([]);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
@ -100,10 +106,12 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: validationError,
|
error: validationError,
|
||||||
};
|
};
|
||||||
setFiles(prev => [...prev, errorFile]);
|
setFiles(prev => mode === 'single' ? [errorFile] : [...prev, errorFile]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUploadStart?.();
|
||||||
|
|
||||||
const fileId = Math.random().toString(36).substr(2, 9);
|
const fileId = Math.random().toString(36).substr(2, 9);
|
||||||
const preview = await createFilePreview(file);
|
const preview = await createFilePreview(file);
|
||||||
|
|
||||||
|
|
@ -117,7 +125,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
};
|
};
|
||||||
|
|
||||||
setFiles(prev => [...prev, newFile]);
|
setFiles(prev => mode === 'single' ? [newFile] : [...prev, newFile]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const progressInterval = setInterval(() => {
|
const progressInterval = setInterval(() => {
|
||||||
|
|
@ -137,14 +145,17 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
? { ...f, progress: 100, status: 'completed', url: result.url, id: result.id }
|
? { ...f, progress: 100, status: 'completed', url: result.url, id: result.id }
|
||||||
: f
|
: f
|
||||||
));
|
));
|
||||||
|
|
||||||
|
onUploadComplete?.();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setFiles(prev => prev.map(f =>
|
setFiles(prev => prev.map(f =>
|
||||||
f.id === fileId
|
f.id === fileId
|
||||||
? { ...f, status: 'error', error: error.message || 'خطا در آپلود فایل' }
|
? { ...f, status: 'error', error: error.message || 'خطا در آپلود فایل' }
|
||||||
: f
|
: f
|
||||||
));
|
));
|
||||||
|
onUploadComplete?.();
|
||||||
}
|
}
|
||||||
}, [onUpload, maxFiles, maxFileSize, acceptedTypes]);
|
}, [onUpload, maxFiles, maxFileSize, acceptedTypes, mode, onUploadStart, onUploadComplete]);
|
||||||
|
|
||||||
const handleFileSelect = useCallback((selectedFiles: FileList) => {
|
const handleFileSelect = useCallback((selectedFiles: FileList) => {
|
||||||
Array.from(selectedFiles).forEach(file => {
|
Array.from(selectedFiles).forEach(file => {
|
||||||
|
|
@ -180,6 +191,9 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
onRemove?.(fileId);
|
onRemove?.(fileId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasUploadedFiles = files.some(f => f.status === 'completed');
|
||||||
|
const showUploadArea = mode === 'multi' || (mode === 'single' && !hasUploadedFiles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-4 ${className}`}>
|
<div className={`space-y-4 ${className}`}>
|
||||||
{label && (
|
{label && (
|
||||||
|
|
@ -188,41 +202,43 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Upload Area */}
|
{/* Upload Area - only show in multi mode or single mode without uploaded files */}
|
||||||
<div
|
{showUploadArea && (
|
||||||
className={`
|
<div
|
||||||
relative border-2 border-dashed rounded-lg p-6 transition-colors cursor-pointer
|
className={`
|
||||||
${isDragOver ? 'border-primary-400 bg-primary-50 dark:bg-primary-900/20' : 'border-gray-300 dark:border-gray-600'}
|
relative border-2 border-dashed rounded-lg p-6 transition-colors cursor-pointer
|
||||||
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700'}
|
${isDragOver ? 'border-primary-400 bg-primary-50 dark:bg-primary-900/20' : 'border-gray-300 dark:border-gray-600'}
|
||||||
${error ? 'border-red-300 bg-red-50 dark:bg-red-900/20' : ''}
|
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700'}
|
||||||
`}
|
${error ? 'border-red-300 bg-red-50 dark:bg-red-900/20' : ''}
|
||||||
onDrop={handleDrop}
|
`}
|
||||||
onDragOver={handleDragOver}
|
onDrop={handleDrop}
|
||||||
onDragLeave={handleDragLeave}
|
onDragOver={handleDragOver}
|
||||||
onClick={handleClick}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
onClick={handleClick}
|
||||||
<input
|
>
|
||||||
ref={fileInputRef}
|
<input
|
||||||
type="file"
|
ref={fileInputRef}
|
||||||
multiple
|
type="file"
|
||||||
accept={acceptedTypes.join(',')}
|
multiple={mode === 'multi'}
|
||||||
className="hidden"
|
accept={acceptedTypes.join(',')}
|
||||||
onChange={(e) => e.target.files && handleFileSelect(e.target.files)}
|
className="hidden"
|
||||||
disabled={disabled}
|
onChange={(e) => e.target.files && handleFileSelect(e.target.files)}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Upload className="mx-auto h-12 w-12 text-gray-400" />
|
<Upload className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||||
حداکثر {formatFileSize(maxFileSize)} • {acceptedTypes.join(', ')}
|
حداکثر {formatFileSize(maxFileSize)} • {acceptedTypes.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
|
<p className="text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ interface VariantManagerProps {
|
||||||
onChange: (variants: ProductVariantFormData[]) => void;
|
onChange: (variants: ProductVariantFormData[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
productOptions?: ProductOption[];
|
productOptions?: ProductOption[];
|
||||||
|
variantAttributeName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VariantFormProps {
|
interface VariantFormProps {
|
||||||
|
|
@ -25,9 +26,10 @@ interface VariantFormProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
productOptions?: ProductOption[];
|
productOptions?: ProductOption[];
|
||||||
|
variantAttributeName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, isEdit = false, productOptions = [] }) => {
|
const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, isEdit = false, productOptions = [], variantAttributeName }) => {
|
||||||
const [formData, setFormData] = useState<ProductVariantFormData>(
|
const [formData, setFormData] = useState<ProductVariantFormData>(
|
||||||
variant || {
|
variant || {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -37,7 +39,6 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
stock_managed: true,
|
stock_managed: true,
|
||||||
stock_number: 0,
|
stock_number: 0,
|
||||||
weight: 0,
|
weight: 0,
|
||||||
product_option_id: undefined,
|
|
||||||
attributes: {},
|
attributes: {},
|
||||||
meta: {},
|
meta: {},
|
||||||
file_ids: []
|
file_ids: []
|
||||||
|
|
@ -49,12 +50,14 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
? variant.file_ids
|
? variant.file_ids
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
const [attributes, setAttributes] = useState<Record<string, any>>(variant?.attributes || {});
|
const [variantAttributeValue, setVariantAttributeValue] = useState('');
|
||||||
const [meta, setMeta] = useState<Record<string, any>>(variant?.meta || {});
|
const [meta, setMeta] = useState<Record<string, any>>(variant?.meta || {});
|
||||||
const [newAttributeKey, setNewAttributeKey] = useState('');
|
|
||||||
const [newAttributeValue, setNewAttributeValue] = useState('');
|
|
||||||
const [newMetaKey, setNewMetaKey] = useState('');
|
const [newMetaKey, setNewMetaKey] = useState('');
|
||||||
const [newMetaValue, setNewMetaValue] = useState('');
|
const [newMetaValue, setNewMetaValue] = useState('');
|
||||||
|
const [attributeError, setAttributeError] = useState('');
|
||||||
|
const [weightDisplay, setWeightDisplay] = useState(variant?.weight?.toString() || '');
|
||||||
|
const [feePercentageDisplay, setFeePercentageDisplay] = useState(variant?.fee_percentage?.toString() || '');
|
||||||
|
const [profitPercentageDisplay, setProfitPercentageDisplay] = useState(variant?.profit_percentage?.toString() || '');
|
||||||
|
|
||||||
const { mutateAsync: uploadFile } = useFileUpload();
|
const { mutateAsync: uploadFile } = useFileUpload();
|
||||||
const { mutate: deleteFile } = useFileDelete();
|
const { mutate: deleteFile } = useFileDelete();
|
||||||
|
|
@ -64,6 +67,23 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
setFormData(prev => ({ ...prev, file_ids: uploadedImages }));
|
setFormData(prev => ({ ...prev, file_ids: uploadedImages }));
|
||||||
}, [uploadedImages]);
|
}, [uploadedImages]);
|
||||||
|
|
||||||
|
// Sync display states with formData when editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (variant?.weight !== undefined) {
|
||||||
|
setWeightDisplay(variant.weight.toString());
|
||||||
|
}
|
||||||
|
if (variant?.fee_percentage !== undefined) {
|
||||||
|
setFeePercentageDisplay(variant.fee_percentage.toString());
|
||||||
|
}
|
||||||
|
if (variant?.profit_percentage !== undefined) {
|
||||||
|
setProfitPercentageDisplay(variant.profit_percentage.toString());
|
||||||
|
}
|
||||||
|
// Load variant attribute value if exists
|
||||||
|
if (variantAttributeName && variant?.attributes && variant.attributes[variantAttributeName]) {
|
||||||
|
setVariantAttributeValue(variant.attributes[variantAttributeName].toString());
|
||||||
|
}
|
||||||
|
}, [variant?.weight, variant?.fee_percentage, variant?.profit_percentage, variant?.attributes, variantAttributeName]);
|
||||||
|
|
||||||
const handleInputChange = (field: keyof ProductVariantFormData, value: any) => {
|
const handleInputChange = (field: keyof ProductVariantFormData, value: any) => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
value = persianToEnglish(value);
|
value = persianToEnglish(value);
|
||||||
|
|
@ -74,15 +94,17 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
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];
|
// Use functional update to avoid stale state when multiple files upload concurrently
|
||||||
setUploadedImages(updatedImages);
|
setUploadedImages(prev => {
|
||||||
|
const newImage: ProductImage = {
|
||||||
|
id: result.id,
|
||||||
|
url: result.url,
|
||||||
|
alt: file.name,
|
||||||
|
order: prev.length
|
||||||
|
};
|
||||||
|
return [...prev, newImage];
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -97,23 +119,7 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
deleteFile(fileId);
|
deleteFile(fileId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAttribute = () => {
|
|
||||||
if (newAttributeKey.trim() && newAttributeValue.trim()) {
|
|
||||||
const updatedAttributes = {
|
|
||||||
...attributes,
|
|
||||||
[newAttributeKey.trim()]: newAttributeValue.trim()
|
|
||||||
};
|
|
||||||
setAttributes(updatedAttributes);
|
|
||||||
setNewAttributeKey('');
|
|
||||||
setNewAttributeValue('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveAttribute = (key: string) => {
|
|
||||||
const updatedAttributes = { ...attributes };
|
|
||||||
delete updatedAttributes[key];
|
|
||||||
setAttributes(updatedAttributes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddMeta = () => {
|
const handleAddMeta = () => {
|
||||||
if (newMetaKey.trim() && newMetaValue.trim()) {
|
if (newMetaKey.trim() && newMetaValue.trim()) {
|
||||||
|
|
@ -134,13 +140,28 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const fileIds = uploadedImages.map(img => Number(img.id)).filter(id => !isNaN(id));
|
// Reset previous errors
|
||||||
|
setAttributeError('');
|
||||||
|
|
||||||
|
// Validate attribute value when attribute name is defined
|
||||||
|
if (variantAttributeName && !variantAttributeValue.trim()) {
|
||||||
|
setAttributeError(`مقدار ${variantAttributeName} الزامی است.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// نگه داشتن آبجکت کامل تصویر برای نمایش در لیست و حالت ویرایش
|
||||||
|
const fileObjects = uploadedImages;
|
||||||
|
|
||||||
|
// Create attributes object with single key-value pair
|
||||||
|
const attributes = variantAttributeName && variantAttributeValue.trim()
|
||||||
|
? { [variantAttributeName]: variantAttributeValue.trim() }
|
||||||
|
: {};
|
||||||
|
|
||||||
const convertedData = convertPersianNumbersInObject({
|
const convertedData = convertPersianNumbersInObject({
|
||||||
...formData,
|
...formData,
|
||||||
attributes,
|
attributes,
|
||||||
meta,
|
meta,
|
||||||
file_ids: fileIds
|
file_ids: fileObjects
|
||||||
});
|
});
|
||||||
|
|
||||||
onSave(convertedData);
|
onSave(convertedData);
|
||||||
|
|
@ -148,18 +169,10 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border">
|
<div className="space-y-6 bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border">
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
{isEdit ? 'ویرایش Variant' : 'افزودن Variant جدید'}
|
{isEdit ? 'ویرایش Variant' : 'افزودن Variant جدید'}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="secondary" onClick={onCancel}>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSave}>
|
|
||||||
{isEdit ? 'بهروزرسانی' : 'افزودن'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Basic Info */}
|
{/* Basic Info */}
|
||||||
|
|
@ -171,10 +184,15 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
value={formData.fee_percentage || ''}
|
value={feePercentageDisplay}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const converted = persianToEnglish(e.target.value);
|
const converted = persianToEnglish(e.target.value);
|
||||||
handleInputChange('fee_percentage', parseFloat(converted) || 0);
|
setFeePercentageDisplay(converted);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const converted = persianToEnglish(e.target.value);
|
||||||
|
const numValue = parseFloat(converted) || 0;
|
||||||
|
handleInputChange('fee_percentage', numValue);
|
||||||
}}
|
}}
|
||||||
className="w-full 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"
|
className="w-full 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"
|
||||||
placeholder="مثال: ۵.۲"
|
placeholder="مثال: ۵.۲"
|
||||||
|
|
@ -188,10 +206,15 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
value={formData.profit_percentage || ''}
|
value={profitPercentageDisplay}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const converted = persianToEnglish(e.target.value);
|
const converted = persianToEnglish(e.target.value);
|
||||||
handleInputChange('profit_percentage', parseFloat(converted) || 0);
|
setProfitPercentageDisplay(converted);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const converted = persianToEnglish(e.target.value);
|
||||||
|
const numValue = parseFloat(converted) || 0;
|
||||||
|
handleInputChange('profit_percentage', numValue);
|
||||||
}}
|
}}
|
||||||
className="w-full 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"
|
className="w-full 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"
|
||||||
placeholder="مثال: ۱۰.۵"
|
placeholder="مثال: ۱۰.۵"
|
||||||
|
|
@ -205,43 +228,23 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
value={formData.weight || ''}
|
value={weightDisplay}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const converted = persianToEnglish(e.target.value);
|
const converted = persianToEnglish(e.target.value);
|
||||||
handleInputChange('weight', parseFloat(converted) || 0);
|
setWeightDisplay(converted);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const converted = persianToEnglish(e.target.value);
|
||||||
|
const numValue = parseFloat(converted) || 0;
|
||||||
|
handleInputChange('weight', numValue);
|
||||||
}}
|
}}
|
||||||
className="w-full 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"
|
className="w-full 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"
|
||||||
placeholder="مثال: ۱۲۰۰"
|
placeholder="مثال: ۱۲۰۰.۵"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Option Selection */}
|
|
||||||
{productOptions && productOptions.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
|
|
||||||
گزینه محصول
|
|
||||||
</h5>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
انتخاب گزینه محصول
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.product_option_id || ''}
|
|
||||||
onChange={(e) => handleInputChange('product_option_id', e.target.value ? parseInt(e.target.value) : undefined)}
|
|
||||||
className="w-full 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"
|
|
||||||
>
|
|
||||||
<option value="">انتخاب کنید...</option>
|
|
||||||
{productOptions.map((option) => (
|
|
||||||
<option key={option.id} value={option.id}>
|
|
||||||
{option.title}
|
|
||||||
{option.description && ` - ${option.description}`}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stock Management */}
|
{/* Stock Management */}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -338,57 +341,29 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attributes */}
|
{/* Variant Attribute */}
|
||||||
<div>
|
{variantAttributeName && (
|
||||||
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
|
<div>
|
||||||
ویژگیهای Variant
|
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
</h5>
|
ویژگی Variant
|
||||||
|
</h5>
|
||||||
<div className="flex gap-3 mb-3">
|
<div>
|
||||||
<input
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
type="text"
|
{variantAttributeName}
|
||||||
value={newAttributeKey}
|
</label>
|
||||||
onChange={(e) => setNewAttributeKey(e.target.value)}
|
<input
|
||||||
placeholder="نام ویژگی (مثل: رنگ، سایز)"
|
type="text"
|
||||||
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"
|
value={variantAttributeValue}
|
||||||
/>
|
onChange={(e) => setVariantAttributeValue(e.target.value)}
|
||||||
<input
|
placeholder={`مقدار ${variantAttributeName} را وارد کنید`}
|
||||||
type="text"
|
className="w-full 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"
|
||||||
value={newAttributeValue}
|
/>
|
||||||
onChange={(e) => setNewAttributeValue(e.target.value)}
|
{attributeError && (
|
||||||
placeholder="مقدار (مثل: قرمز، بزرگ)"
|
<p className="text-red-500 text-xs mt-1">{attributeError}</p>
|
||||||
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>
|
|
||||||
|
|
||||||
{Object.keys(attributes).length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
||||||
{Object.entries(attributes).map(([key, value]) => (
|
|
||||||
<div key={key} className="flex items-center justify-between bg-white dark:bg-gray-600 px-3 py-2 rounded-md border">
|
|
||||||
<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-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Meta Data */}
|
{/* Meta Data */}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -454,11 +429,21 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
||||||
Variant فعال باشد
|
Variant فعال باشد
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<Button variant="secondary" onClick={onCancel}>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
{isEdit ? 'بهروزرسانی' : 'افزودن'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChange, disabled = false, productOptions = [] }) => {
|
export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChange, disabled = false, productOptions = [], variantAttributeName }) => {
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|
@ -518,6 +503,7 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
|
||||||
onCancel={handleCancelForm}
|
onCancel={handleCancelForm}
|
||||||
isEdit={editingIndex !== null}
|
isEdit={editingIndex !== null}
|
||||||
productOptions={productOptions}
|
productOptions={productOptions}
|
||||||
|
variantAttributeName={variantAttributeName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -549,13 +535,8 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
|
||||||
<strong>موجودی:</strong> {variant.stock_managed ? `${variant.stock_number} عدد` : 'بدون محدودیت'}
|
<strong>موجودی:</strong> {variant.stock_managed ? `${variant.stock_number} عدد` : 'بدون محدودیت'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>وزن:</strong> {variant.weight} گرم
|
<strong>وزن:</strong> {parseFloat(variant.weight.toString()).toLocaleString('fa-IR')} گرم
|
||||||
</div>
|
</div>
|
||||||
{variant.product_option_id && (
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<strong>گزینه محصول:</strong> {productOptions.find(opt => opt.id === variant.product_option_id)?.title || `شناسه ${variant.product_option_id}`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{variant.file_ids && variant.file_ids.length > 0 && (
|
{variant.file_ids && variant.file_ids.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { ArrowRight, FolderOpen } from 'lucide-react';
|
||||||
import { Button } from '../../../components/ui/Button';
|
import { Button } from '../../../components/ui/Button';
|
||||||
import { Input } from '../../../components/ui/Input';
|
import { Input } from '../../../components/ui/Input';
|
||||||
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
|
||||||
|
import { FileUploader } from '../../../components/ui/FileUploader';
|
||||||
|
import { useFileUpload, useFileDelete } from '../../../hooks/useFileUpload';
|
||||||
import { useToast } from '../../../contexts/ToastContext';
|
import { useToast } from '../../../contexts/ToastContext';
|
||||||
import { useCategory, useCreateCategory, useUpdateCategory } from '../core/_hooks';
|
import { useCategory, useCreateCategory, useUpdateCategory } from '../core/_hooks';
|
||||||
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
|
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
|
||||||
|
|
@ -18,8 +20,12 @@ const CategoryFormPage = () => {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
parent_id: null as number | null,
|
parent_id: null as number | null,
|
||||||
|
file_id: undefined as number | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [uploadedImage, setUploadedImage] = useState<{ id: string, url: string } | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const { data: category, isLoading: isLoadingCategory } = useCategory(
|
const { data: category, isLoading: isLoadingCategory } = useCategory(
|
||||||
id || '0',
|
id || '0',
|
||||||
isEdit
|
isEdit
|
||||||
|
|
@ -27,6 +33,8 @@ const CategoryFormPage = () => {
|
||||||
|
|
||||||
const createMutation = useCreateCategory();
|
const createMutation = useCreateCategory();
|
||||||
const updateMutation = useUpdateCategory();
|
const updateMutation = useUpdateCategory();
|
||||||
|
const { mutateAsync: uploadFile } = useFileUpload();
|
||||||
|
const { mutate: deleteFile } = useFileDelete();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (category && isEdit) {
|
if (category && isEdit) {
|
||||||
|
|
@ -34,7 +42,16 @@ const CategoryFormPage = () => {
|
||||||
name: category.name || '',
|
name: category.name || '',
|
||||||
description: category.description || '',
|
description: category.description || '',
|
||||||
parent_id: category.parent_id || null,
|
parent_id: category.parent_id || null,
|
||||||
|
file_id: category.file_id || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set uploaded image if exists
|
||||||
|
if (category.file_id) {
|
||||||
|
setUploadedImage({
|
||||||
|
id: category.file_id.toString(),
|
||||||
|
url: '' // We don't have URL from category, just ID
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [category, isEdit]);
|
}, [category, isEdit]);
|
||||||
|
|
||||||
|
|
@ -45,6 +62,34 @@ const CategoryFormPage = () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (file: File) => {
|
||||||
|
try {
|
||||||
|
const result = await uploadFile(file);
|
||||||
|
const fileId = parseInt(result.id);
|
||||||
|
setUploadedImage({
|
||||||
|
id: result.id,
|
||||||
|
url: result.url
|
||||||
|
});
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
file_id: fileId
|
||||||
|
}));
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileRemove = (fileId: string) => {
|
||||||
|
setUploadedImage(null);
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
file_id: undefined
|
||||||
|
}));
|
||||||
|
deleteFile(fileId);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -120,6 +165,21 @@ const CategoryFormPage = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FileUploader
|
||||||
|
onUpload={handleFileUpload}
|
||||||
|
onRemove={handleFileRemove}
|
||||||
|
acceptedTypes={['image/*']}
|
||||||
|
maxFileSize={5 * 1024 * 1024} // 5MB
|
||||||
|
maxFiles={1}
|
||||||
|
mode="single"
|
||||||
|
label="تصویر دستهبندی"
|
||||||
|
description="تصویر دستهبندی را انتخاب کنید (حداکثر 5MB)"
|
||||||
|
onUploadStart={() => setIsUploading(true)}
|
||||||
|
onUploadComplete={() => setIsUploading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:justify-end sm:space-y-0 sm:space-x-3 sm:space-x-reverse pt-4 border-t border-gray-200 dark:border-gray-700">
|
<div className="flex flex-col space-y-3 sm:flex-row sm:justify-end sm:space-y-0 sm:space-x-3 sm:space-x-reverse pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -133,6 +193,7 @@ const CategoryFormPage = () => {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={createMutation.isPending || updateMutation.isPending}
|
loading={createMutation.isPending || updateMutation.isPending}
|
||||||
|
disabled={isUploading}
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
{isEdit ? 'ویرایش' : 'ایجاد'}
|
{isEdit ? 'ویرایش' : 'ایجاد'}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export interface Category {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
parent_id?: number;
|
parent_id?: number;
|
||||||
|
file_id?: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +11,7 @@ export interface Category {
|
||||||
export interface CategoryFormData {
|
export interface CategoryFormData {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
file_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryFilters {
|
export interface CategoryFilters {
|
||||||
|
|
@ -21,12 +23,14 @@ export interface CategoryFilters {
|
||||||
export interface CreateCategoryRequest {
|
export interface CreateCategoryRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
file_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateCategoryRequest {
|
export interface UpdateCategoryRequest {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
file_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoriesResponse {
|
export interface CategoriesResponse {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useForm, useFieldArray } from 'react-hook-form';
|
import { useForm, useFieldArray } from 'react-hook-form';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
@ -8,6 +8,8 @@ import { ProductOptionFormData, Maintenance, Option } from '../core/_models';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/components/ui/Input";
|
||||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||||
|
import { FileUploader } from "@/components/ui/FileUploader";
|
||||||
|
import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload";
|
||||||
import { ArrowRight, Settings, Plus, Trash2 } from "lucide-react";
|
import { ArrowRight, Settings, Plus, Trash2 } from "lucide-react";
|
||||||
import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography';
|
import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography';
|
||||||
|
|
||||||
|
|
@ -39,6 +41,11 @@ const ProductOptionFormPage = () => {
|
||||||
const { data: productOption, isLoading: isLoadingOption } = useProductOption(id || '', isEdit);
|
const { data: productOption, isLoading: isLoadingOption } = useProductOption(id || '', isEdit);
|
||||||
const { mutate: createOption, isPending: isCreating } = useCreateProductOption();
|
const { mutate: createOption, isPending: isCreating } = useCreateProductOption();
|
||||||
const { mutate: updateOption, isPending: isUpdating } = useUpdateProductOption();
|
const { mutate: updateOption, isPending: isUpdating } = useUpdateProductOption();
|
||||||
|
const { mutateAsync: uploadFile } = useFileUpload();
|
||||||
|
const { mutate: deleteFile } = useFileDelete();
|
||||||
|
|
||||||
|
const [uploadedImage, setUploadedImage] = useState<{ id: string, url: string } | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const isLoading = isCreating || isUpdating;
|
const isLoading = isCreating || isUpdating;
|
||||||
|
|
||||||
|
|
@ -78,6 +85,14 @@ const ProductOptionFormPage = () => {
|
||||||
setValue('description', productOption.description, { shouldValidate: true });
|
setValue('description', productOption.description, { shouldValidate: true });
|
||||||
setValue('maintenance', productOption.maintenance, { shouldValidate: true });
|
setValue('maintenance', productOption.maintenance, { shouldValidate: true });
|
||||||
setValue('options', productOption.options, { shouldValidate: true });
|
setValue('options', productOption.options, { shouldValidate: true });
|
||||||
|
|
||||||
|
// Set uploaded image if exists
|
||||||
|
if (productOption.maintenance.image) {
|
||||||
|
setUploadedImage({
|
||||||
|
id: productOption.maintenance.image,
|
||||||
|
url: productOption.maintenance.image
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isEdit, productOption, setValue]);
|
}, [isEdit, productOption, setValue]);
|
||||||
|
|
||||||
|
|
@ -108,6 +123,28 @@ const ProductOptionFormPage = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (file: File) => {
|
||||||
|
try {
|
||||||
|
const result = await uploadFile(file);
|
||||||
|
const imageData = {
|
||||||
|
id: result.id,
|
||||||
|
url: result.url
|
||||||
|
};
|
||||||
|
setUploadedImage(imageData);
|
||||||
|
setValue('maintenance.image', result.id, { shouldValidate: true, shouldDirty: true });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileRemove = (fileId: string) => {
|
||||||
|
setUploadedImage(null);
|
||||||
|
setValue('maintenance.image', '', { shouldValidate: true, shouldDirty: true });
|
||||||
|
deleteFile(fileId);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoadingOption) {
|
if (isLoadingOption) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
|
@ -151,42 +188,65 @@ const ProductOptionFormPage = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Label htmlFor="description">توضیحات</Label>
|
||||||
label="توضیحات"
|
<textarea
|
||||||
|
id="description"
|
||||||
{...register('description')}
|
{...register('description')}
|
||||||
error={errors.description?.message}
|
|
||||||
placeholder="توضیحات گزینه محصول را وارد کنید"
|
placeholder="توضیحات گزینه محصول را وارد کنید"
|
||||||
|
rows={4}
|
||||||
|
className="input resize-none"
|
||||||
/>
|
/>
|
||||||
|
{errors.description?.message && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400">{errors.description.message}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
<SectionTitle className="mb-4">اطلاعات نگهداری</SectionTitle>
|
<SectionTitle className="mb-4">اطلاعات نگهداری</SectionTitle>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
label="عنوان نگهداری"
|
<Input
|
||||||
{...register('maintenance.title')}
|
label="عنوان نگهداری"
|
||||||
error={errors.maintenance?.title?.message}
|
{...register('maintenance.title')}
|
||||||
placeholder="عنوان نگهداری را وارد کنید"
|
error={errors.maintenance?.title?.message}
|
||||||
/>
|
placeholder="عنوان نگهداری را وارد کنید"
|
||||||
<Input
|
/>
|
||||||
label="توضیحات نگهداری"
|
<Input
|
||||||
{...register('maintenance.description')}
|
label="توضیحات نگهداری"
|
||||||
error={errors.maintenance?.description?.message}
|
{...register('maintenance.description')}
|
||||||
placeholder="توضیحات نگهداری را وارد کنید"
|
error={errors.maintenance?.description?.message}
|
||||||
/>
|
placeholder="توضیحات نگهداری را وارد کنید"
|
||||||
<Input
|
/>
|
||||||
label="محتوای نگهداری"
|
</div>
|
||||||
{...register('maintenance.content')}
|
<div>
|
||||||
error={errors.maintenance?.content?.message}
|
<Label htmlFor="maintenance-content">محتوای نگهداری</Label>
|
||||||
placeholder="محتوای نگهداری را وارد کنید"
|
<textarea
|
||||||
/>
|
id="maintenance-content"
|
||||||
<Input
|
{...register('maintenance.content')}
|
||||||
label="تصویر نگهداری"
|
placeholder="محتوای نگهداری را وارد کنید"
|
||||||
{...register('maintenance.image')}
|
rows={4}
|
||||||
error={errors.maintenance?.image?.message}
|
className="input resize-none"
|
||||||
placeholder="آدرس تصویر نگهداری را وارد کنید"
|
/>
|
||||||
/>
|
{errors.maintenance?.content?.message && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400">{errors.maintenance.content.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FileUploader
|
||||||
|
onUpload={handleFileUpload}
|
||||||
|
onRemove={handleFileRemove}
|
||||||
|
acceptedTypes={['image/*']}
|
||||||
|
maxFileSize={5 * 1024 * 1024} // 5MB
|
||||||
|
maxFiles={1}
|
||||||
|
mode="single"
|
||||||
|
label="تصویر نگهداری"
|
||||||
|
description="تصویر نگهداری را انتخاب کنید (حداکثر 5MB)"
|
||||||
|
error={errors.maintenance?.image?.message}
|
||||||
|
onUploadStart={() => setIsUploading(true)}
|
||||||
|
onUploadComplete={() => setIsUploading(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -218,25 +278,34 @@ const ProductOptionFormPage = () => {
|
||||||
حذف
|
حذف
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
label="عنوان"
|
<Input
|
||||||
{...register(`options.${index}.title`)}
|
label="عنوان"
|
||||||
error={errors.options?.[index]?.title?.message}
|
{...register(`options.${index}.title`)}
|
||||||
placeholder="عنوان گزینه را وارد کنید"
|
error={errors.options?.[index]?.title?.message}
|
||||||
/>
|
placeholder="عنوان گزینه را وارد کنید"
|
||||||
<Input
|
/>
|
||||||
label="توضیحات"
|
<Input
|
||||||
{...register(`options.${index}.description`)}
|
label="متا تایتل"
|
||||||
error={errors.options?.[index]?.description?.message}
|
{...register(`options.${index}.meta_title`)}
|
||||||
placeholder="توضیحات گزینه را وارد کنید"
|
error={errors.options?.[index]?.meta_title?.message}
|
||||||
/>
|
placeholder="متا تایتل را وارد کنید"
|
||||||
<Input
|
/>
|
||||||
label="متا تایتل"
|
</div>
|
||||||
{...register(`options.${index}.meta_title`)}
|
<div>
|
||||||
error={errors.options?.[index]?.meta_title?.message}
|
<Label htmlFor={`option-${index}-description`}>توضیحات</Label>
|
||||||
placeholder="متا تایتل را وارد کنید"
|
<textarea
|
||||||
/>
|
id={`option-${index}-description`}
|
||||||
|
{...register(`options.${index}.description`)}
|
||||||
|
placeholder="توضیحات گزینه را وارد کنید"
|
||||||
|
rows={3}
|
||||||
|
className="input resize-none"
|
||||||
|
/>
|
||||||
|
{errors.options?.[index]?.description?.message && (
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400">{errors.options[index]?.description?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -260,7 +329,7 @@ const ProductOptionFormPage = () => {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!isValid || isLoading}
|
disabled={!isValid || isLoading || isUploading}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
{isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول'}
|
{isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول'}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ const ProductFormPage = () => {
|
||||||
const [attributes, setAttributes] = useState<Record<string, any>>({});
|
const [attributes, setAttributes] = useState<Record<string, any>>({});
|
||||||
const [newAttributeKey, setNewAttributeKey] = useState('');
|
const [newAttributeKey, setNewAttributeKey] = useState('');
|
||||||
const [newAttributeValue, setNewAttributeValue] = useState('');
|
const [newAttributeValue, setNewAttributeValue] = useState('');
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const { data: product, isLoading: isLoadingProduct } = useProduct(id || '', isEdit);
|
const { data: product, isLoading: isLoadingProduct } = useProduct(id || '', isEdit);
|
||||||
const { data: categories, isLoading: isLoadingCategories } = useCategories();
|
const { data: categories, isLoading: isLoadingCategories } = useCategories();
|
||||||
|
|
@ -391,7 +392,7 @@ const ProductFormPage = () => {
|
||||||
|
|
||||||
<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">
|
||||||
گزینه محصول
|
ویژگی های محصول و نحوه نگه داری
|
||||||
</label>
|
</label>
|
||||||
{isLoadingProductOptions ? (
|
{isLoadingProductOptions ? (
|
||||||
<div className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
|
<div className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -432,8 +433,11 @@ const ProductFormPage = () => {
|
||||||
acceptedTypes={['image/*']}
|
acceptedTypes={['image/*']}
|
||||||
maxFileSize={5 * 1024 * 1024}
|
maxFileSize={5 * 1024 * 1024}
|
||||||
maxFiles={10}
|
maxFiles={10}
|
||||||
|
mode="multi"
|
||||||
label=""
|
label=""
|
||||||
description="تصاویر محصول را اینجا بکشید یا کلیک کنید"
|
description="تصاویر محصول را اینجا بکشید یا کلیک کنید"
|
||||||
|
onUploadStart={() => setIsUploading(true)}
|
||||||
|
onUploadComplete={() => setIsUploading(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{uploadedImages.length > 0 && (
|
{uploadedImages.length > 0 && (
|
||||||
|
|
@ -613,7 +617,7 @@ const ProductFormPage = () => {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={!isValid || isLoading}
|
disabled={!isValid || isLoading || isUploading}
|
||||||
>
|
>
|
||||||
{isEdit ? 'بهروزرسانی' : 'ایجاد محصول'}
|
{isEdit ? 'بهروزرسانی' : 'ایجاد محصول'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue