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