fix(product-options): update API routes and enhance product option models
This commit is contained in:
parent
58ecd2e94a
commit
b3c3a8afd0
|
|
@ -1,4 +1,5 @@
|
|||
import { clsx } from 'clsx';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
interface ButtonProps {
|
||||
children: any;
|
||||
|
|
@ -6,7 +7,7 @@ interface ButtonProps {
|
|||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: (e?: MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
className?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
|||
setFiles(prev => [...prev, newFile]);
|
||||
|
||||
try {
|
||||
// شبیهسازی پروگرس
|
||||
const progressInterval = setInterval(() => {
|
||||
setFiles(prev => prev.map(f =>
|
||||
f.id === fileId && f.progress < 90
|
||||
|
|
@ -145,13 +144,13 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
|||
: f
|
||||
));
|
||||
}
|
||||
}, [onUpload, files.length, maxFiles, maxFileSize, acceptedTypes]);
|
||||
}, [onUpload, maxFiles, maxFileSize, acceptedTypes]);
|
||||
|
||||
const handleFileSelect = (selectedFiles: FileList) => {
|
||||
const handleFileSelect = useCallback((selectedFiles: FileList) => {
|
||||
Array.from(selectedFiles).forEach(file => {
|
||||
handleFileUpload(file);
|
||||
});
|
||||
};
|
||||
}, [handleFileUpload]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -160,7 +159,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
|||
|
||||
const droppedFiles = e.dataTransfer.files;
|
||||
handleFileSelect(droppedFiles);
|
||||
}, [disabled, handleFileUpload]);
|
||||
}, [disabled, handleFileSelect]);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -307,7 +306,7 @@ export const FileUploader: React.FC<FileUploaderProps> = ({
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e?.stopPropagation();
|
||||
handleRemove(file.id);
|
||||
}}
|
||||
className="p-1 h-8 w-8"
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ export const API_ROUTES = {
|
|||
// Product Options APIs
|
||||
GET_PRODUCT_OPTIONS: "api/v1/product-options",
|
||||
GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
|
||||
CREATE_PRODUCT_OPTION: "api/v1/products/options",
|
||||
UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/products/options/${id}`,
|
||||
CREATE_PRODUCT_OPTION: "api/v1/product-options",
|
||||
UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
|
||||
DELETE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
|
||||
|
||||
// Categories APIs
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
httpPostRequest,
|
||||
httpDeleteRequest,
|
||||
APIUrlGenerator,
|
||||
} from "@/utils/baseHttpService";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { APIUrlGenerator } from "@/utils/baseHttpService";
|
||||
import { API_ROUTES } from "@/constant/routes";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface UploadFileResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
url: string;
|
||||
serve_key: string;
|
||||
created_at: string;
|
||||
}
|
||||
import { httpPostRequest, httpDeleteRequest } from "@/utils/baseHttpService";
|
||||
|
||||
interface UploadResponse {
|
||||
file: UploadFileResponse;
|
||||
file: {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
original_name: string;
|
||||
serve_key: string;
|
||||
size: number;
|
||||
mime_type: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const useFileUpload = () => {
|
||||
|
|
@ -26,12 +23,18 @@ export const useFileUpload = () => {
|
|||
mutationFn: async (file: File): Promise<{ id: string; url: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("name", "uploaded-file");
|
||||
|
||||
console.log("Uploading file:", file.name);
|
||||
|
||||
const response = await httpPostRequest<UploadResponse>(
|
||||
APIUrlGenerator(API_ROUTES.UPLOAD_FILE),
|
||||
formData
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Upload response:", response);
|
||||
|
|
@ -41,7 +44,7 @@ export const useFileUpload = () => {
|
|||
}
|
||||
|
||||
return {
|
||||
id: response.data.file.id,
|
||||
id: response.data.file.id.toString(),
|
||||
url: response.data.file.url,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,31 @@
|
|||
export interface Maintenance {
|
||||
content: string;
|
||||
description: string;
|
||||
image: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
description: string;
|
||||
meta_title: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ProductOption {
|
||||
id: number;
|
||||
name: string;
|
||||
values: string[];
|
||||
title: string;
|
||||
description: string;
|
||||
maintenance: Maintenance;
|
||||
options: Option[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProductOptionFormData {
|
||||
name: string;
|
||||
values: string[];
|
||||
title: string;
|
||||
description: string;
|
||||
maintenance: Maintenance;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
export interface ProductOptionFilters {
|
||||
|
|
@ -18,14 +35,18 @@ export interface ProductOptionFilters {
|
|||
}
|
||||
|
||||
export interface CreateProductOptionRequest {
|
||||
name: string;
|
||||
values: string[];
|
||||
title: string;
|
||||
description: string;
|
||||
maintenance: Maintenance;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
export interface UpdateProductOptionRequest {
|
||||
id: number;
|
||||
name: string;
|
||||
values: string[];
|
||||
title: string;
|
||||
description: string;
|
||||
maintenance: Maintenance;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
export interface ProductOptionsResponse {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,33 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
import { useProductOption, useCreateProductOption, useUpdateProductOption } from '../core/_hooks';
|
||||
import { ProductOptionFormData } from '../core/_models';
|
||||
import { ProductOptionFormData, Maintenance, Option } from '../core/_models';
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||
import { TagInput } from "@/components/ui/TagInput";
|
||||
import { ArrowRight, Settings } from "lucide-react";
|
||||
import { ArrowRight, Settings, Plus, Trash2 } from "lucide-react";
|
||||
|
||||
const maintenanceSchema = yup.object({
|
||||
title: yup.string().required('عنوان نگهداری الزامی است'),
|
||||
description: yup.string().required('توضیحات نگهداری الزامی است'),
|
||||
content: yup.string().required('محتوای نگهداری الزامی است'),
|
||||
image: yup.string().required('تصویر نگهداری الزامی است'),
|
||||
});
|
||||
|
||||
const optionSchema = yup.object({
|
||||
title: yup.string().required('عنوان گزینه الزامی است'),
|
||||
description: yup.string().required('توضیحات گزینه الزامی است'),
|
||||
meta_title: yup.string().required('متا تایتل الزامی است'),
|
||||
});
|
||||
|
||||
const productOptionSchema = yup.object({
|
||||
name: yup.string().required('نام گزینه الزامی است').min(2, 'نام گزینه باید حداقل 2 کاراکتر باشد'),
|
||||
values: yup.array().of(yup.string()).min(1, 'حداقل یک مقدار باید وارد شود').required('مقادیر الزامی است'),
|
||||
title: yup.string().required('عنوان الزامی است').min(2, 'عنوان باید حداقل 2 کاراکتر باشد'),
|
||||
description: yup.string().required('توضیحات الزامی است'),
|
||||
maintenance: maintenanceSchema.required('اطلاعات نگهداری الزامی است'),
|
||||
options: yup.array().of(optionSchema).min(1, 'حداقل یک گزینه باید وارد شود').required('گزینهها الزامی است'),
|
||||
});
|
||||
|
||||
const ProductOptionFormPage = () => {
|
||||
|
|
@ -30,7 +44,7 @@ const ProductOptionFormPage = () => {
|
|||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isValid, isDirty },
|
||||
formState: { errors, isValid },
|
||||
setValue,
|
||||
watch,
|
||||
control
|
||||
|
|
@ -38,17 +52,31 @@ const ProductOptionFormPage = () => {
|
|||
resolver: yupResolver(productOptionSchema) as any,
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
name: '',
|
||||
values: []
|
||||
title: '',
|
||||
description: '',
|
||||
maintenance: {
|
||||
title: '',
|
||||
description: '',
|
||||
content: '',
|
||||
image: ''
|
||||
},
|
||||
options: []
|
||||
}
|
||||
});
|
||||
|
||||
const { fields: optionFields, append: appendOption, remove: removeOption } = useFieldArray({
|
||||
control,
|
||||
name: "options"
|
||||
});
|
||||
|
||||
const formValues = watch();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && productOption) {
|
||||
setValue('name', productOption.name, { shouldValidate: true });
|
||||
setValue('values', productOption.values, { shouldValidate: true });
|
||||
setValue('title', productOption.title, { shouldValidate: true });
|
||||
setValue('description', productOption.description, { shouldValidate: true });
|
||||
setValue('maintenance', productOption.maintenance, { shouldValidate: true });
|
||||
setValue('options', productOption.options, { shouldValidate: true });
|
||||
}
|
||||
}, [isEdit, productOption, setValue]);
|
||||
|
||||
|
|
@ -56,18 +84,14 @@ const ProductOptionFormPage = () => {
|
|||
if (isEdit && id) {
|
||||
updateOption({
|
||||
id: parseInt(id),
|
||||
name: data.name,
|
||||
values: data.values
|
||||
...data
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
navigate('/product-options');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createOption({
|
||||
name: data.name,
|
||||
values: data.values
|
||||
}, {
|
||||
createOption(data, {
|
||||
onSuccess: () => {
|
||||
navigate('/product-options');
|
||||
}
|
||||
|
|
@ -75,122 +99,172 @@ const ProductOptionFormPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/product-options');
|
||||
const addOption = () => {
|
||||
appendOption({
|
||||
title: '',
|
||||
description: '',
|
||||
meta_title: ''
|
||||
});
|
||||
};
|
||||
|
||||
const handleValuesChange = (values: string[]) => {
|
||||
setValue('values', values, { shouldValidate: true, shouldDirty: true });
|
||||
};
|
||||
|
||||
if (isEdit && isLoadingOption) {
|
||||
if (isLoadingOption) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
onClick={() => navigate('/product-options')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
بازگشت
|
||||
برگشت
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<Settings className="h-6 w-6" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-6 w-6 text-primary-600" />
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول جدید'}
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
{isEdit ? 'ویرایش اطلاعات گزینه محصول' : 'اطلاعات گزینه محصول جدید را وارد کنید'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-6 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Input
|
||||
label="نام گزینه"
|
||||
{...register('name')}
|
||||
error={errors.name?.message}
|
||||
placeholder="مثال: رنگ، سایز، جنس"
|
||||
label="عنوان"
|
||||
{...register('title')}
|
||||
error={errors.title?.message}
|
||||
placeholder="عنوان گزینه محصول را وارد کنید"
|
||||
/>
|
||||
|
||||
<TagInput
|
||||
label="مقادیر گزینه"
|
||||
values={watch('values') || []}
|
||||
onChange={handleValuesChange}
|
||||
placeholder="مقدار جدید اضافه کنید..."
|
||||
error={errors.values?.message}
|
||||
/>
|
||||
|
||||
{/* Preview */}
|
||||
{formValues.values && formValues.values.length > 0 && (
|
||||
<div className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
پیشنمایش گزینه
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>نام:</strong> {formValues.name || 'نام گزینه'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>مقادیر:</strong>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{formValues.values.map((value, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200"
|
||||
<div>
|
||||
<Input
|
||||
label="توضیحات"
|
||||
{...register('description')}
|
||||
error={errors.description?.message}
|
||||
placeholder="توضیحات گزینه محصول را وارد کنید"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">اطلاعات نگهداری</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="عنوان نگهداری"
|
||||
{...register('maintenance.title')}
|
||||
error={errors.maintenance?.title?.message}
|
||||
placeholder="عنوان نگهداری را وارد کنید"
|
||||
/>
|
||||
<Input
|
||||
label="توضیحات نگهداری"
|
||||
{...register('maintenance.description')}
|
||||
error={errors.maintenance?.description?.message}
|
||||
placeholder="توضیحات نگهداری را وارد کنید"
|
||||
/>
|
||||
<Input
|
||||
label="محتوای نگهداری"
|
||||
{...register('maintenance.content')}
|
||||
error={errors.maintenance?.content?.message}
|
||||
placeholder="محتوای نگهداری را وارد کنید"
|
||||
/>
|
||||
<Input
|
||||
label="تصویر نگهداری"
|
||||
{...register('maintenance.image')}
|
||||
error={errors.maintenance?.image?.message}
|
||||
placeholder="آدرس تصویر نگهداری را وارد کنید"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">گزینهها</h3>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={addOption}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<Plus className="h-4 w-4" />
|
||||
افزودن گزینه
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{optionFields.map((field, index) => (
|
||||
<div key={field.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-md font-medium text-gray-900 dark:text-gray-100">گزینه {index + 1}</h4>
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={() => removeOption(index)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Input
|
||||
label="عنوان"
|
||||
{...register(`options.${index}.title`)}
|
||||
error={errors.options?.[index]?.title?.message}
|
||||
placeholder="عنوان گزینه را وارد کنید"
|
||||
/>
|
||||
<Input
|
||||
label="توضیحات"
|
||||
{...register(`options.${index}.description`)}
|
||||
error={errors.options?.[index]?.description?.message}
|
||||
placeholder="توضیحات گزینه را وارد کنید"
|
||||
/>
|
||||
<Input
|
||||
label="متا تایتل"
|
||||
{...register(`options.${index}.meta_title`)}
|
||||
error={errors.options?.[index]?.meta_title?.message}
|
||||
placeholder="متا تایتل را وارد کنید"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{optionFields.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
هیچ گزینهای تعریف نشده است. برای شروع گزینهای اضافه کنید.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-4 space-x-reverse pt-6 border-t border-gray-200 dark:border-gray-600">
|
||||
<div className="flex justify-end gap-4 pt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
onClick={() => navigate('/product-options')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
انصراف
|
||||
لغو
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
variant="primary"
|
||||
disabled={!isValid || isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isEdit ? 'بهروزرسانی' : 'ایجاد'}
|
||||
{isEdit ? 'ویرایش گزینه محصول' : 'ایجاد گزینه محصول'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
|
||||
راهنما
|
||||
</h3>
|
||||
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>• گزینههای محصول برای تعریف ویژگیهایی مثل رنگ، سایز، جنس استفاده میشوند</li>
|
||||
<li>• هر گزینه میتواند چندین مقدار داشته باشد (مثل قرمز، آبی، سبز برای رنگ)</li>
|
||||
<li>• این گزینهها بعداً در ایجاد محصولات قابل استفاده خواهند بود</li>
|
||||
<li>• برای اضافه کردن مقدار جدید، آن را تایپ کنید و Enter بزنید</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -163,22 +163,22 @@ const ProductOptionsListPage = () => {
|
|||
{(productOptions || []).map((option: ProductOption) => (
|
||||
<tr key={option.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{option.name}
|
||||
{option.title}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{option.values.slice(0, 3).map((value, index) => (
|
||||
{(option.options || []).slice(0, 3).map((optionItem, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<Tag className="h-3 w-3 mr-1" />
|
||||
{value}
|
||||
{optionItem.title}
|
||||
</span>
|
||||
))}
|
||||
{option.values.length > 3 && (
|
||||
{(option.options || []).length > 3 && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
|
||||
+{option.values.length - 3} بیشتر
|
||||
+{(option.options || []).length - 3} بیشتر
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -218,21 +218,21 @@ const ProductOptionsListPage = () => {
|
|||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{option.name}
|
||||
{option.title}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{option.values.slice(0, 3).map((value, index) => (
|
||||
{(option.options || []).slice(0, 3).map((optionItem, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-gray-100 dark:bg-gray-600 text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<Tag className="h-3 w-3 mr-1" />
|
||||
{value}
|
||||
{optionItem.title}
|
||||
</span>
|
||||
))}
|
||||
{option.values.length > 3 && (
|
||||
{(option.options || []).length > 3 && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-md text-xs bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
|
||||
+{option.values.length - 3} بیشتر
|
||||
+{(option.options || []).length - 3} بیشتر
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -195,8 +195,8 @@ const ProductFormPage = () => {
|
|||
|
||||
const productOptionOptions = (productOptions || []).map(option => ({
|
||||
id: option.id,
|
||||
title: option.name,
|
||||
description: `مقادیر: ${option.values.join(', ')}`
|
||||
title: option.title,
|
||||
description: `تعداد گزینهها: ${(option.options || []).length}`
|
||||
}));
|
||||
|
||||
return (
|
||||
|
|
@ -329,7 +329,7 @@ const ProductFormPage = () => {
|
|||
<option value="">بدون گزینه</option>
|
||||
{(productOptions || []).map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.name} ({option.values.join(', ')})
|
||||
{option.title} ({(option.options || []).length} گزینه)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
791
swagger.json
791
swagger.json
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue