feat: enhance input handling and product management features
- Added numeric input handling in Input component with Persian to English conversion - Updated MultiSelectAutocomplete styles for dark mode compatibility - Enhanced VariantManager to support product options and improved variant data handling - Updated ProductDetailPage and ProductFormPage to display and manage product options - Improved ProductsListPage to handle numeric filters with Persian number conversion
This commit is contained in:
parent
e00eb4a160
commit
4f4ef51ccc
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { Label } from './Typography';
|
||||
import { persianToEnglish } from '../../utils/numberUtils';
|
||||
|
||||
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
label?: string;
|
||||
|
|
@ -8,10 +9,11 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, '
|
|||
helperText?: string;
|
||||
inputSize?: 'sm' | 'md' | 'lg';
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
numeric?: boolean;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, helperText, inputSize = 'md', className, id, ...props }, ref) => {
|
||||
({ label, error, helperText, inputSize = 'md', className, id, onChange, type, numeric, ...props }, ref) => {
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-2 text-sm',
|
||||
md: 'px-3 py-3 text-base',
|
||||
|
|
@ -29,15 +31,35 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
className
|
||||
);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if ((type === 'number' || numeric) && e.target.value) {
|
||||
const convertedValue = persianToEnglish(e.target.value);
|
||||
e.target.value = convertedValue;
|
||||
}
|
||||
onChange?.(e);
|
||||
};
|
||||
|
||||
const getInputMode = (): "numeric" | "decimal" | undefined => {
|
||||
if (numeric) {
|
||||
return type === 'number' ? 'decimal' : 'numeric';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const inputProps = {
|
||||
ref,
|
||||
id,
|
||||
type: numeric ? 'text' : type,
|
||||
inputMode: getInputMode(),
|
||||
className: inputClasses,
|
||||
onChange: handleChange,
|
||||
...props
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && <Label htmlFor={id}>{label}</Label>}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={inputClasses}
|
||||
{...props}
|
||||
/>
|
||||
<input {...inputProps} />
|
||||
{helperText && !error && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{helperText}</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export const MultiSelectAutocomplete: React.FC<MultiSelectAutocompleteProps> = (
|
|||
selectedOptions.map(option => (
|
||||
<span
|
||||
key={option.id}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary-100 text-primary-800 text-xs rounded-md"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary-100 dark:bg-primary-800 text-primary-800 dark:text-primary-100 text-xs rounded-md"
|
||||
>
|
||||
{option.title}
|
||||
<button
|
||||
|
|
@ -107,7 +107,7 @@ export const MultiSelectAutocomplete: React.FC<MultiSelectAutocompleteProps> = (
|
|||
e.stopPropagation();
|
||||
handleRemoveOption(option.id);
|
||||
}}
|
||||
className="hover:bg-primary-200 rounded-full p-0.5"
|
||||
className="hover:bg-primary-200 dark:hover:bg-primary-700 rounded-full p-0.5 transition-colors"
|
||||
disabled={disabled}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Plus, Trash2, Edit3, Package } from 'lucide-react';
|
||||
import { Plus, Trash2, Edit3, Package, X, Edit, Image as ImageIcon } from 'lucide-react';
|
||||
import { ProductVariantFormData, ProductImage } from '../../pages/products/core/_models';
|
||||
import { Button } from './Button';
|
||||
|
||||
import { FileUploader } from './FileUploader';
|
||||
import { useFileUpload, useFileDelete } from '../../hooks/useFileUpload';
|
||||
import { persianToEnglish, convertPersianNumbersInObject } from '../../utils/numberUtils';
|
||||
|
||||
interface ProductOption {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface VariantManagerProps {
|
||||
variants: ProductVariantFormData[];
|
||||
onChange: (variants: ProductVariantFormData[]) => void;
|
||||
disabled?: boolean;
|
||||
productOptions?: ProductOption[];
|
||||
}
|
||||
|
||||
interface VariantFormProps {
|
||||
|
|
@ -17,9 +24,10 @@ interface VariantFormProps {
|
|||
onSave: (variant: ProductVariantFormData) => void;
|
||||
onCancel: () => void;
|
||||
isEdit?: boolean;
|
||||
productOptions?: ProductOption[];
|
||||
}
|
||||
|
||||
const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, isEdit = false }) => {
|
||||
const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, isEdit = false, productOptions = [] }) => {
|
||||
const [formData, setFormData] = useState<ProductVariantFormData>(
|
||||
variant || {
|
||||
enabled: true,
|
||||
|
|
@ -29,6 +37,7 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
stock_managed: true,
|
||||
stock_number: 0,
|
||||
weight: 0,
|
||||
product_option_id: undefined,
|
||||
attributes: {},
|
||||
meta: {},
|
||||
images: []
|
||||
|
|
@ -47,6 +56,9 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
const { mutate: deleteFile } = useFileDelete();
|
||||
|
||||
const handleInputChange = (field: keyof ProductVariantFormData, value: any) => {
|
||||
if (typeof value === 'string') {
|
||||
value = persianToEnglish(value);
|
||||
}
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
|
|
@ -113,13 +125,14 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const variantToSave: ProductVariantFormData = {
|
||||
const convertedData = convertPersianNumbersInObject({
|
||||
...formData,
|
||||
images: uploadedImages,
|
||||
attributes,
|
||||
meta
|
||||
};
|
||||
onSave(variantToSave);
|
||||
meta,
|
||||
images: uploadedImages
|
||||
});
|
||||
|
||||
onSave(convertedData);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -145,14 +158,15 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
درصد کارمزد
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.fee_percentage}
|
||||
onChange={(e) => handleInputChange('fee_percentage', parseFloat(e.target.value) || 0)}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={formData.fee_percentage || ''}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
handleInputChange('fee_percentage', parseFloat(converted) || 0);
|
||||
}}
|
||||
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="0"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
placeholder="مثال: ۵.۲"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -161,14 +175,15 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
درصد سود
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.profit_percentage}
|
||||
onChange={(e) => handleInputChange('profit_percentage', parseFloat(e.target.value) || 0)}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={formData.profit_percentage || ''}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
handleInputChange('profit_percentage', parseFloat(converted) || 0);
|
||||
}}
|
||||
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="0"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
placeholder="مثال: ۱۰.۵"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -177,17 +192,46 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
وزن (گرم)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.weight}
|
||||
onChange={(e) => handleInputChange('weight', parseFloat(e.target.value) || 0)}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={formData.weight || ''}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
handleInputChange('weight', parseFloat(converted) || 0);
|
||||
}}
|
||||
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="0"
|
||||
min="0"
|
||||
step="0.1"
|
||||
placeholder="مثال: ۱۲۰۰"
|
||||
/>
|
||||
</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 */}
|
||||
<div>
|
||||
<h5 className="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
|
|
@ -211,27 +255,33 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
تعداد موجودی
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.stock_number}
|
||||
onChange={(e) => handleInputChange('stock_number', parseInt(e.target.value) || 0)}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={formData.stock_number || ''}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
handleInputChange('stock_number', parseInt(converted) || 0);
|
||||
}}
|
||||
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="0"
|
||||
min="0"
|
||||
placeholder="مثال: ۱۰۰"
|
||||
disabled={!formData.stock_managed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
حد کمینه موجودی
|
||||
حد موجودی
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.stock_limit}
|
||||
onChange={(e) => handleInputChange('stock_limit', parseInt(e.target.value) || 0)}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={formData.stock_limit || ''}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
handleInputChange('stock_limit', parseInt(converted) || 0);
|
||||
}}
|
||||
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="0"
|
||||
min="0"
|
||||
placeholder="مثال: ۱۰"
|
||||
disabled={!formData.stock_managed}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -397,7 +447,7 @@ const VariantForm: React.FC<VariantFormProps> = ({ variant, onSave, onCancel, is
|
|||
);
|
||||
};
|
||||
|
||||
export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChange, disabled = false }) => {
|
||||
export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChange, disabled = false, productOptions = [] }) => {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
|
||||
|
|
@ -456,6 +506,7 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
|
|||
onSave={handleSaveVariant}
|
||||
onCancel={handleCancelForm}
|
||||
isEdit={editingIndex !== null}
|
||||
productOptions={productOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -489,6 +540,11 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
|
|||
<div>
|
||||
<strong>وزن:</strong> {variant.weight} گرم
|
||||
</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>
|
||||
|
||||
{variant.images && variant.images.length > 0 && (
|
||||
|
|
@ -527,6 +583,7 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
|
|||
{!disabled && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEditVariant(index)}
|
||||
className="p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-md"
|
||||
title="ویرایش"
|
||||
|
|
@ -534,6 +591,7 @@ export const VariantManager: React.FC<VariantManagerProps> = ({ variants, onChan
|
|||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteVariant(index)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md"
|
||||
title="حذف"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface ProductVariant {
|
|||
stock_managed: boolean;
|
||||
stock_number: number;
|
||||
weight: number;
|
||||
product_option_id?: number;
|
||||
attributes?: Record<string, any>;
|
||||
meta?: Record<string, any>;
|
||||
images?: ProductImage[];
|
||||
|
|
@ -33,6 +34,7 @@ export interface Product {
|
|||
enabled: boolean;
|
||||
category_ids?: number[];
|
||||
categories?: Category[];
|
||||
product_categories?: Category[];
|
||||
category?: Category;
|
||||
product_option_id?: number;
|
||||
product_option?: ProductOption;
|
||||
|
|
@ -44,6 +46,7 @@ export interface Product {
|
|||
attributes?: Record<string, any>;
|
||||
images: ProductImage[];
|
||||
variants?: ProductVariant[];
|
||||
product_variants?: ProductVariant[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
|
@ -71,6 +74,7 @@ export interface ProductVariantFormData {
|
|||
stock_managed: boolean;
|
||||
stock_number: number;
|
||||
weight: number;
|
||||
product_option_id?: number;
|
||||
attributes: Record<string, any>;
|
||||
meta: Record<string, any>;
|
||||
images: ProductImage[];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowRight, Edit, Package, Tag, Image, Calendar, FileText, Eye, DollarSign } 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 { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
|
||||
import { useProduct } from '../core/_hooks';
|
||||
import { PRODUCT_TYPE_LABELS } from '../core/_models';
|
||||
import { PageContainer, PageTitle, SectionTitle } from '../../../components/ui/Typography';
|
||||
|
||||
const ProductDetailPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -19,55 +20,75 @@ const ProductDetailPage = () => {
|
|||
return new Intl.NumberFormat('fa-IR').format(price) + ' تومان';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate('/products')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
بازگشت
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
جزئیات محصول
|
||||
</h1>
|
||||
</div>
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('fa-IR').format(num);
|
||||
};
|
||||
|
||||
<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>
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate('/products')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
بازگشت
|
||||
</Button>
|
||||
<div>
|
||||
<PageTitle>جزئیات محصول</PageTitle>
|
||||
<p className="text-gray-600 dark:text-gray-400">{product.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/products/${id}/edit`)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
ویرایش
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* اطلاعات اصلی */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-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" />
|
||||
اطلاعات محصول
|
||||
</h2>
|
||||
</SectionTitle>
|
||||
|
||||
<div className="space-y-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 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>
|
||||
<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 && (
|
||||
|
|
@ -96,7 +117,7 @@ const ProductDetailPage = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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">
|
||||
نوع محصول
|
||||
|
|
@ -121,16 +142,54 @@ const ProductDetailPage = () => {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.price && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
قیمت
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{formatPrice(product.price)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ویژگیهای محصول */}
|
||||
{product.attributes && Object.keys(product.attributes).length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Settings className="h-5 w-5" />
|
||||
ویژگیهای محصول
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(product.attributes).map(([key, value]) => (
|
||||
<div key={key} className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{key}
|
||||
</span>
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* تصاویر محصول */}
|
||||
{product.images && product.images.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Image className="h-5 w-5" />
|
||||
تصاویر محصول
|
||||
</h3>
|
||||
</SectionTitle>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{product.images.map((image, index) => (
|
||||
<div key={image.id || index} className="relative group">
|
||||
|
|
@ -148,44 +207,137 @@ const ProductDetailPage = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* محصول متغیر */}
|
||||
{/* نسخههای محصول */}
|
||||
{product.variants && product.variants.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
<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" />
|
||||
نسخههای محصول
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
</SectionTitle>
|
||||
<div className="space-y-6">
|
||||
{product.variants.map((variant, index) => (
|
||||
<div key={variant.id || index} className="p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">وضعیت:</span>
|
||||
<span className={`ml-2 px-2 py-1 text-xs rounded-full ${variant.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||
}`}>
|
||||
{variant.enabled ? 'فعال' : 'غیرفعال'}
|
||||
<div key={variant.id || index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-gray-100">
|
||||
نسخه {index + 1}
|
||||
</h4>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${variant.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||
}`}>
|
||||
{variant.enabled ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block">درصد کارمزد</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{formatNumber(variant.fee_percentage)}%
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">موجودی:</span>
|
||||
<span className="ml-2 font-medium text-gray-900 dark:text-gray-100">
|
||||
{variant.stock_number}
|
||||
<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>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">وزن:</span>
|
||||
<span className="ml-2 font-medium text-gray-900 dark:text-gray-100">
|
||||
{variant.weight} گرم
|
||||
<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>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">درصد سود:</span>
|
||||
<span className="ml-2 font-medium text-gray-900 dark:text-gray-100">
|
||||
{variant.profit_percentage}%
|
||||
<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.images && variant.images.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.images.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>
|
||||
|
|
@ -196,11 +348,24 @@ const ProductDetailPage = () => {
|
|||
{/* اطلاعات جانبی */}
|
||||
<div className="space-y-6">
|
||||
{/* آمار */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
آمار
|
||||
</h3>
|
||||
<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" />
|
||||
|
|
@ -209,20 +374,20 @@ const ProductDetailPage = () => {
|
|||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{product.total_sold || 0}
|
||||
{formatNumber(product.total_sold || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{product.variants && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-blue-500" />
|
||||
<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">
|
||||
{product.variants.length}
|
||||
{formatNumber(product.variants.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -236,7 +401,21 @@ const ProductDetailPage = () => {
|
|||
</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{product.images.length}
|
||||
{formatNumber(product.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>
|
||||
)}
|
||||
|
|
@ -245,20 +424,26 @@ const ProductDetailPage = () => {
|
|||
|
||||
{/* دستهبندیها */}
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<SectionTitle className="flex items-center gap-2 mb-4">
|
||||
<Tag className="h-5 w-5" />
|
||||
دستهبندیها
|
||||
</h3>
|
||||
</SectionTitle>
|
||||
<div className="space-y-2">
|
||||
{product.categories.map((category) => (
|
||||
<div
|
||||
key={category.id}
|
||||
className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
className="flex items-center gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"
|
||||
>
|
||||
<Tag className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-blue-900 dark:text-blue-100 font-medium">
|
||||
{category.name}
|
||||
</span>
|
||||
{category.description && (
|
||||
<span className="text-xs text-blue-700 dark:text-blue-300">
|
||||
- {category.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -267,13 +452,14 @@ const ProductDetailPage = () => {
|
|||
|
||||
{/* گزینه محصول */}
|
||||
{product.product_option && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
<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" />
|
||||
گزینه محصول
|
||||
</h3>
|
||||
</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.name}
|
||||
{product.product_option.title}
|
||||
</p>
|
||||
{product.product_option.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
|
|
@ -285,10 +471,11 @@ const ProductDetailPage = () => {
|
|||
)}
|
||||
|
||||
{/* اطلاعات زمانی */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
<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" />
|
||||
اطلاعات زمانی
|
||||
</h3>
|
||||
</SectionTitle>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
|
|
@ -317,7 +504,7 @@ const ProductDetailPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,16 +16,20 @@ import { FileUploader } from "@/components/ui/FileUploader";
|
|||
import { VariantManager } from "@/components/ui/VariantManager";
|
||||
import { ArrowRight, Package, X, Plus, Trash2 } from "lucide-react";
|
||||
import { FormHeader, PageContainer, SectionTitle, Label } from '../../../components/ui/Typography';
|
||||
import { createNumberTransform, createOptionalNumberTransform, convertPersianNumbersInObject } from '../../../utils/numberUtils';
|
||||
|
||||
const productSchema = yup.object({
|
||||
name: yup.string().required('نام محصول الزامی است').min(2, 'نام محصول باید حداقل 2 کاراکتر باشد'),
|
||||
description: yup.string().default(''),
|
||||
design_style: yup.string().default(''),
|
||||
description: yup.string(),
|
||||
design_style: yup.string(),
|
||||
enabled: yup.boolean().default(true),
|
||||
total_sold: yup.number().min(0).default(0),
|
||||
type: yup.number().oneOf([0, 1, 2, 3]).default(0),
|
||||
total_sold: yup.number()
|
||||
.transform(createNumberTransform())
|
||||
.min(0, 'تعداد فروخته شده نمیتواند منفی باشد')
|
||||
.optional(),
|
||||
type: yup.number().oneOf([0, 1, 2, 3]).default(1),
|
||||
category_ids: yup.array().of(yup.number()).default([]),
|
||||
product_option_id: yup.number().optional().nullable(),
|
||||
product_option_id: yup.number().transform(createOptionalNumberTransform()).nullable(),
|
||||
attributes: yup.object().default({}),
|
||||
images: yup.array().of(yup.object()).default([]),
|
||||
variants: yup.array().default([]),
|
||||
|
|
@ -67,8 +71,8 @@ const ProductFormPage = () => {
|
|||
description: '',
|
||||
design_style: '',
|
||||
enabled: true,
|
||||
total_sold: 0,
|
||||
type: 1, // هارد کد شده به VARIABLE
|
||||
total_sold: undefined,
|
||||
type: 1,
|
||||
category_ids: [],
|
||||
product_option_id: undefined,
|
||||
attributes: {},
|
||||
|
|
@ -81,8 +85,23 @@ const ProductFormPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (isEdit && product) {
|
||||
// تبدیل variants از ProductVariant به ProductVariantFormData
|
||||
const formVariants = product.variants?.map(variant => ({
|
||||
console.log('Product data in edit mode:', product);
|
||||
|
||||
// Extract category IDs - handle different API response formats
|
||||
let categoryIds: number[] = [];
|
||||
if (product.category_ids && product.category_ids.length > 0) {
|
||||
categoryIds = product.category_ids;
|
||||
} else if (product.categories && product.categories.length > 0) {
|
||||
categoryIds = product.categories.map(cat => cat.id);
|
||||
} else if (product.product_categories && product.product_categories.length > 0) {
|
||||
categoryIds = product.product_categories.map(cat => cat.id);
|
||||
}
|
||||
|
||||
console.log('✅ Successfully extracted category IDs:', categoryIds);
|
||||
|
||||
// تبدیل variants از ProductVariant به ProductVariantFormData - handle both variants and product_variants
|
||||
const variants = product.variants || product.product_variants || [];
|
||||
const formVariants = variants.map(variant => ({
|
||||
id: variant.id,
|
||||
enabled: variant.enabled,
|
||||
fee_percentage: variant.fee_percentage,
|
||||
|
|
@ -91,10 +110,13 @@ const ProductFormPage = () => {
|
|||
stock_managed: variant.stock_managed,
|
||||
stock_number: variant.stock_number,
|
||||
weight: variant.weight,
|
||||
product_option_id: variant.product_option_id || undefined,
|
||||
attributes: variant.attributes || {},
|
||||
meta: variant.meta || {},
|
||||
images: variant.images || []
|
||||
})) || [];
|
||||
}));
|
||||
|
||||
console.log('✅ Successfully processed variants:', formVariants.length);
|
||||
|
||||
reset({
|
||||
name: product.name,
|
||||
|
|
@ -102,8 +124,8 @@ const ProductFormPage = () => {
|
|||
design_style: product.design_style || '',
|
||||
enabled: product.enabled,
|
||||
total_sold: product.total_sold || 0,
|
||||
type: 1, // هارد کد شده به VARIABLE
|
||||
category_ids: product.category_ids || [],
|
||||
type: 1,
|
||||
category_ids: categoryIds,
|
||||
product_option_id: product.product_option_id || undefined,
|
||||
attributes: product.attributes || {},
|
||||
images: product.images || [],
|
||||
|
|
@ -162,25 +184,36 @@ const ProductFormPage = () => {
|
|||
setValue('attributes', updatedAttributes, { shouldValidate: true, shouldDirty: true });
|
||||
};
|
||||
|
||||
const onSubmit = (data: ProductFormData) => {
|
||||
const onSubmit = (data: any) => {
|
||||
const convertedData = convertPersianNumbersInObject(data);
|
||||
|
||||
const validImageIds = uploadedImages
|
||||
.map(img => {
|
||||
const numericId = Number(img.id);
|
||||
return isNaN(numericId) ? null : numericId;
|
||||
})
|
||||
.filter(id => id !== null);
|
||||
|
||||
const baseSubmitData = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
design_style: data.design_style,
|
||||
enabled: data.enabled,
|
||||
total_sold: data.total_sold,
|
||||
type: 1, // هارد کد شده به VARIABLE
|
||||
attributes,
|
||||
category_ids: data.category_ids.length > 0 ? data.category_ids : [],
|
||||
product_option_id: data.product_option_id || undefined,
|
||||
images: uploadedImages.map(img => parseInt(img.id)) // فقط ID های تصاویر به صورت عدد ارسال میشود
|
||||
name: convertedData.name,
|
||||
description: convertedData.description || '',
|
||||
design_style: convertedData.design_style || '',
|
||||
enabled: convertedData.enabled,
|
||||
total_sold: convertedData.total_sold || 0,
|
||||
type: 1,
|
||||
attributes: convertPersianNumbersInObject(attributes),
|
||||
category_ids: convertedData.category_ids.length > 0 ? convertedData.category_ids : [],
|
||||
product_option_id: convertedData.product_option_id || null,
|
||||
images: validImageIds
|
||||
};
|
||||
|
||||
console.log('Submitting product data:', baseSubmitData);
|
||||
console.log('Original data:', data);
|
||||
console.log('Converted data:', convertedData);
|
||||
|
||||
if (isEdit && id) {
|
||||
// برای update، variants باید شامل ID باشه
|
||||
const updateVariants = data.variants?.map(variant => ({
|
||||
const updateVariants = data.variants?.map((variant: any) => ({
|
||||
id: variant.id || 0, // اگر ID نداره، 0 بذار (برای variant جدید)
|
||||
enabled: variant.enabled,
|
||||
fee_percentage: variant.fee_percentage,
|
||||
|
|
@ -189,6 +222,7 @@ const ProductFormPage = () => {
|
|||
stock_managed: variant.stock_managed,
|
||||
stock_number: variant.stock_number,
|
||||
weight: variant.weight,
|
||||
product_option_id: variant.product_option_id || null,
|
||||
attributes: variant.attributes && Object.keys(variant.attributes).length > 0 ? variant.attributes : {},
|
||||
meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {}
|
||||
})) || [];
|
||||
|
|
@ -204,7 +238,7 @@ const ProductFormPage = () => {
|
|||
});
|
||||
} else {
|
||||
// برای create، variants نباید ID داشته باشه
|
||||
const createVariants = data.variants?.map(variant => ({
|
||||
const createVariants = data.variants?.map((variant: any) => ({
|
||||
enabled: variant.enabled,
|
||||
fee_percentage: variant.fee_percentage,
|
||||
profit_percentage: variant.profit_percentage,
|
||||
|
|
@ -212,6 +246,7 @@ const ProductFormPage = () => {
|
|||
stock_managed: variant.stock_managed,
|
||||
stock_number: variant.stock_number,
|
||||
weight: variant.weight,
|
||||
product_option_id: variant.product_option_id || null,
|
||||
attributes: variant.attributes && Object.keys(variant.attributes).length > 0 ? variant.attributes : {},
|
||||
meta: variant.meta && Object.keys(variant.meta).length > 0 ? variant.meta : {}
|
||||
})) || [];
|
||||
|
|
@ -251,6 +286,8 @@ const ProductFormPage = () => {
|
|||
description: `تعداد گزینهها: ${(option.options || []).length}`
|
||||
}));
|
||||
|
||||
|
||||
|
||||
const backButton = (
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -303,11 +340,10 @@ const ProductFormPage = () => {
|
|||
|
||||
<Input
|
||||
label="تعداد فروخته شده"
|
||||
type="number"
|
||||
numeric
|
||||
{...register('total_sold')}
|
||||
error={errors.total_sold?.message}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
placeholder="مثال: ۱۰۰"
|
||||
/>
|
||||
|
||||
<Input
|
||||
|
|
@ -353,17 +389,26 @@ const ProductFormPage = () => {
|
|||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
گزینه محصول
|
||||
</label>
|
||||
<select
|
||||
{...register('product_option_id')}
|
||||
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.options || []).length} گزینه)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{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>
|
||||
) : (
|
||||
<select
|
||||
{...register('product_option_id')}
|
||||
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>
|
||||
{productOptionOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.title} - {option.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{productOptionOptions.length === 0 && !isLoadingProductOptions && (
|
||||
<p className="text-amber-600 text-sm mt-1">هیچ گزینه محصولی یافت نشد</p>
|
||||
)}
|
||||
{errors.product_option_id && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.product_option_id.message}</p>
|
||||
)}
|
||||
|
|
@ -424,6 +469,7 @@ const ProductFormPage = () => {
|
|||
<VariantManager
|
||||
variants={watch('variants') || []}
|
||||
onChange={(variants) => setValue('variants', variants, { shouldValidate: true, shouldDirty: true })}
|
||||
productOptions={productOptionOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Button } from "@/components/ui/Button";
|
|||
|
||||
import { Trash2, Edit3, Plus, Package, Eye, Image } from "lucide-react";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { persianToEnglish } from '../../../utils/numberUtils';
|
||||
|
||||
const ProductsTableSkeleton = () => (
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
|
|
@ -220,17 +221,25 @@ const ProductsListPage = () => {
|
|||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="حداقل"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="حداقل (مثال: ۱۰۰۰۰)"
|
||||
value={filters.min_price}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, min_price: e.target.value }))}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
setFilters(prev => ({ ...prev, min_price: converted }));
|
||||
}}
|
||||
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"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="حداکثر"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="حداکثر (مثال: ۵۰۰۰۰۰)"
|
||||
value={filters.max_price}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, max_price: e.target.value }))}
|
||||
onChange={(e) => {
|
||||
const converted = persianToEnglish(e.target.value);
|
||||
setFilters(prev => ({ ...prev, max_price: converted }));
|
||||
}}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
export const persianToEnglish = (str: string | number): string => {
|
||||
if (typeof str === "number") return str.toString();
|
||||
if (!str) return "";
|
||||
|
||||
const persianNumbers = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
||||
const arabicNumbers = ["٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"];
|
||||
const englishNumbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||||
|
||||
let result = str.toString();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result = result.replace(
|
||||
new RegExp(persianNumbers[i], "g"),
|
||||
englishNumbers[i]
|
||||
);
|
||||
result = result.replace(
|
||||
new RegExp(arabicNumbers[i], "g"),
|
||||
englishNumbers[i]
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const convertPersianNumbersInObject = (obj: any): any => {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
|
||||
if (typeof obj === "string") {
|
||||
return persianToEnglish(obj);
|
||||
}
|
||||
|
||||
if (typeof obj === "number") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => convertPersianNumbersInObject(item));
|
||||
}
|
||||
|
||||
if (typeof obj === "object") {
|
||||
const converted: any = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
converted[key] = convertPersianNumbersInObject(obj[key]);
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const createNumberTransform = () => {
|
||||
return (value: any, originalValue: any) => {
|
||||
if (
|
||||
originalValue === "" ||
|
||||
originalValue === null ||
|
||||
originalValue === undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const converted = persianToEnglish(originalValue);
|
||||
const num = Number(converted);
|
||||
|
||||
return isNaN(num) ? undefined : num;
|
||||
};
|
||||
};
|
||||
|
||||
export const createOptionalNumberTransform = () => {
|
||||
return (value: any, originalValue: any) => {
|
||||
if (
|
||||
originalValue === "" ||
|
||||
originalValue === null ||
|
||||
originalValue === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const converted = persianToEnglish(originalValue);
|
||||
const num = Number(converted);
|
||||
|
||||
return isNaN(num) ? null : num;
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue