request create product + category learning

This commit is contained in:
hamid zarghami
2025-10-12 11:38:23 +03:30
parent db6157ae5d
commit 72f84d00b0
10 changed files with 403 additions and 229 deletions
+99 -3
View File
@@ -1,8 +1,104 @@
import { type FC } from 'react'
import { type FC, useState } from 'react'
import { useGetLearningCategory } from './hooks/useSellerData'
import { type LearningCategory } from './types/Types'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import Td from '../../components/Td'
import Button from '@/components/Button'
import CategoryLearningTableRow from './components/CategoryLearningTableRow'
import CreateUpdateCategoryModal from './components/CreateUpdateCategoryModal'
const CategoryLearning: FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false)
const [selectedCategory, setSelectedCategory] = useState<LearningCategory | undefined>()
const { data: categoriesData, isLoading, error, refetch } = useGetLearningCategory()
const handleModalClose = () => {
setIsModalOpen(false)
setSelectedCategory(undefined)
}
const handleModalSuccess = () => {
refetch()
}
const handleEditCategory = (category: LearningCategory) => {
setSelectedCategory(category)
setIsModalOpen(true)
}
const handleCreateCategory = () => {
setSelectedCategory(undefined)
setIsModalOpen(true)
}
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری دسته‌بندی‌های یادگیری" />
</div>
)
}
const categories = categoriesData?.results?.learningCategory || []
const CategoryLearning = () => {
return (
<div>CategoryLearning</div>
<div>
<div className='flex justify-end mt-5'>
<Button
label="ایجاد دسته‌بندی جدید"
onClick={handleCreateCategory}
className="w-auto"
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text={'عنوان'} />
<Td text={'تاریخ ایجاد'} />
<Td text={'تاریخ بروزرسانی'} />
<Td text={'وضعیت'} />
<Td text={'عملیات'} />
</tr>
</thead>
<tbody>
{categories.length === 0 ? (
<tr className='tr'>
<td colSpan={5} className="text-center py-8 text-gray-500">
هیچ دستهبندی یادگیری یافت نشد
</td>
</tr>
) : (
categories.map((category: LearningCategory) => (
<CategoryLearningTableRow
key={category._id}
category={category}
onEdit={handleEditCategory}
/>
))
)}
</tbody>
</table>
</div>
<CreateUpdateCategoryModal
open={isModalOpen}
close={handleModalClose}
onSuccess={handleModalSuccess}
category={selectedCategory}
/>
</div>
)
}
@@ -0,0 +1,63 @@
import { type FC } from 'react'
import Td from '../../../components/Td'
import { Calendar, Edit } from 'iconsax-react'
import StatusWithText from '../../../components/StatusWithText'
import type { LearningCategory } from '../types/Types'
interface Props {
category: LearningCategory
onEdit: (category: LearningCategory) => void
}
const CategoryLearningTableRow: FC<Props> = ({ category, onEdit }) => {
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
return (
<tr className='tr'>
<Td text="">
<div className="max-w-xs">
<div className="font-medium text-gray-900 truncate">
{category.title}
</div>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Calendar size={16} color="#8C90A3" />
<span className="text-gray-600 text-sm">
{formatDate(category.createdAt)}
</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Calendar size={16} color="#8C90A3" />
<span className="text-gray-600 text-sm">
{formatDate(category.updatedAt)}
</span>
</div>
</Td>
<Td text="">
<StatusWithText
variant={category.deleted ? 'error' : 'success'}
text={category.deleted ? 'غیرفعال' : 'فعال'}
/>
</Td>
<Td text="">
<div className="flex gap-2">
<Edit onClick={() => onEdit(category)} color="#8C90A3" size={16} />
</div>
</Td>
</tr>
)
}
export default CategoryLearningTableRow
@@ -0,0 +1,111 @@
import { type FC, useEffect } from 'react'
import DefaulModal from '@/components/DefaulModal'
import Input from '@/components/Input'
import Button from '@/components/Button'
import { useCreateLearningCategory, useUpdateLearningCategory } from '../hooks/useSellerData'
import { type LearningCategory, type CreateCategoryLearning } from '../types/Types'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils'
interface Props {
open: boolean
close: () => void
onSuccess?: () => void
category?: LearningCategory
}
const CreateUpdateCategoryModal: FC<Props> = ({ open, close, onSuccess, category }) => {
const isEdit = !!category
const createMutation = useCreateLearningCategory()
const updateMutation = useUpdateLearningCategory()
const formik = useFormik<CreateCategoryLearning>({
initialValues: {
title: category?.title || ''
},
validationSchema: Yup.object({
title: Yup.string().required('عنوان دسته‌بندی الزامی است')
}),
onSubmit: (values) => {
if (isEdit && category) {
// Update category
updateMutation.mutate({
id: category._id,
params: values
}, {
onSuccess: () => {
toast.success('دسته‌بندی با موفقیت بروزرسانی شد')
close()
onSuccess?.()
formik.resetForm()
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
} else {
// Create category
createMutation.mutate(values, {
onSuccess: () => {
toast.success('دسته‌بندی جدید با موفقیت ایجاد شد')
close()
onSuccess?.()
formik.resetForm()
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
}
}
})
useEffect(() => {
if (open) {
formik.resetForm()
formik.setValues({
title: category?.title || ''
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, category])
return (
<DefaulModal
open={open}
close={close}
isHeader={true}
title_header={isEdit ? "ویرایش دسته‌بندی یادگیری" : "ایجاد دسته‌بندی یادگیری جدید"}
width={500}
>
<div className="space-y-6 mt-4 w-[450px]">
<Input
label="عنوان دسته‌بندی"
placeholder="عنوان دسته‌بندی را وارد کنید"
{...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
<div className="flex gap-3 pt-4">
<Button
label="لغو"
variant="outline"
onClick={close}
className="flex-1"
/>
<Button
label={isEdit ? "بروزرسانی" : "ایجاد"}
onClick={() => formik.handleSubmit()}
isLoading={createMutation.isPending || updateMutation.isPending}
disabled={!formik.isValid || createMutation.isPending || updateMutation.isPending}
className="flex-1"
/>
</div>
</div>
</DefaulModal>
)
}
export default CreateUpdateCategoryModal
@@ -1,8 +1,6 @@
import { type FC } from 'react'
import DefaulModal from '@/components/DefaulModal'
import { type ProductRequest } from '../types/Types'
import StatusWithText from '@/components/StatusWithText'
import { Calendar, User as UserIcon, Shop, Tag, Box, MessageSquare, Location, Money } from 'iconsax-react'
interface Props {
open: boolean
@@ -13,31 +11,6 @@ interface Props {
const ProductRequestDetailModal: FC<Props> = ({ open, close, request }) => {
if (!request) return null
const getStatusVariant = (status: string) => {
switch (status) {
case 'Approved': return 'success'
case 'Rejected': return 'error'
case 'Pending': return 'warning'
case 'Draft': return 'warning'
default: return 'warning'
}
}
const getStatusText = (status: string) => {
switch (status) {
case 'Approved': return 'تایید شده'
case 'Rejected': return 'رد شده'
case 'Pending': return 'در انتظار'
case 'Draft': return 'پیش‌نویس'
default: return status
}
}
const formatDate = (dateString: string) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleDateString('fa-IR')
}
return (
<DefaulModal
open={open}
@@ -47,203 +20,10 @@ const ProductRequestDetailModal: FC<Props> = ({ open, close, request }) => {
width={800}
>
<div className="space-y-6 mt-4">
{/* وضعیت درخواست */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">وضعیت درخواست</h3>
<StatusWithText
variant={getStatusVariant(request.requestStatus)}
text={getStatusText(request.requestStatus)}
/>
</div>
{/* اطلاعات فروشنده */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<UserIcon size={20} color="#374151" />
اطلاعات فروشنده
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-2">
<UserIcon size={16} color="#6B7280" />
<span className="text-sm text-gray-600">نام:</span>
<span className="text-sm font-medium">{request.seller?.fullName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Shop size={16} color="#6B7280" />
<span className="text-sm text-gray-600">فروشگاه:</span>
<span className="text-sm font-medium">{request.shop?.shopName || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Location size={16} color="#6B7280" />
<span className="text-sm text-gray-600">آدرس:</span>
<span className="text-sm font-medium">{request.shop?.address?.province?.name || '-'}، {request.shop?.address?.city?.name || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Tag size={16} color="#6B7280" />
<span className="text-sm text-gray-600">کد فروشگاه:</span>
<span className="text-sm font-medium">{request.shop?.shopCode || '-'}</span>
</div>
</div>
</div>
{/* اطلاعات محصول */}
<div className="bg-blue-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<Box size={20} color="#1E40AF" />
اطلاعات محصول
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-2">
<Box size={16} color="#6B7280" />
<span className="text-sm text-gray-600">نام محصول:</span>
<span className="text-sm font-medium">{request.productName}</span>
</div>
<div className="flex items-center gap-2">
<Tag size={16} color="#6B7280" />
<span className="text-sm text-gray-600">برند:</span>
<span className="text-sm font-medium">{request.brand?.title_fa || '-'}</span>
</div>
<div className="flex items-center gap-2">
<MessageSquare size={16} color="#6B7280" />
<span className="text-sm text-gray-600">دستهبندی:</span>
<span className="text-sm font-medium">{request.category?.title_fa || '-'}</span>
</div>
<div className="flex items-center gap-2">
<Tag size={16} color="#6B7280" />
<span className="text-sm text-gray-600">منبع:</span>
<span className="text-sm font-medium">{request.source || '-'}</span>
</div>
</div>
</div>
{/* توضیحات */}
<div className="bg-green-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3">توضیحات</h4>
<p className="text-sm text-gray-700 leading-relaxed">{String(request.description || 'توضیحاتی وارد نشده')}</p>
</div>
{/* ابعاد و وزن */}
<div className="bg-yellow-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<Box size={20} color="#92400E" />
ابعاد و وزن
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_weight || 0}</div>
<div className="text-xs text-gray-600">وزن (گرم)</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_length || 0}</div>
<div className="text-xs text-gray-600">طول (سانتیمتر)</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_width || 0}</div>
<div className="text-xs text-gray-600">عرض (سانتیمتر)</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">{request.dimensions?.package_height || 0}</div>
<div className="text-xs text-gray-600">ارتفاع (سانتیمتر)</div>
</div>
</div>
</div>
{/* مزایا و معایب */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-green-50 rounded-lg p-4">
<h4 className="text-md font-medium text-green-900 mb-3">مزایا</h4>
{request.advantages && request.advantages.length > 0 ? (
<ul className="space-y-1">
{request.advantages.map((advantage, index) => (
<li key={index} className="text-sm text-green-800 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-green-600 rounded-full"></span>
{String(advantage)}
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-600">مزایایی وارد نشده</p>
)}
</div>
<div className="bg-red-50 rounded-lg p-4">
<h4 className="text-md font-medium text-red-900 mb-3">معایب</h4>
{request.disAdvantages && request.disAdvantages.length > 0 ? (
<ul className="space-y-1">
{request.disAdvantages.map((disadvantage, index) => (
<li key={index} className="text-sm text-red-800 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-red-600 rounded-full"></span>
{String(disadvantage)}
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-600">معایبی وارد نشده</p>
)}
</div>
</div>
{/* خدمات درخواست شده */}
<div className="bg-purple-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3">خدمات درخواست شده</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={request.requestPhotography}
readOnly
className="rounded border-gray-300"
/>
<span className="text-sm">📸 عکاسی محصول</span>
{request.requestPhotosCount && (
<span className="text-xs text-gray-600">({request.requestPhotosCount} عکس)</span>
)}
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={request.expertReview}
readOnly
className="rounded border-gray-300"
/>
<span className="text-sm">👨💼 بررسی تخصصی</span>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={request.unboxingVideo}
readOnly
className="rounded border-gray-300"
/>
<span className="text-sm">📦 ویدیو باز کردن بسته</span>
</div>
</div>
</div>
{/* اطلاعات پرداخت */}
{request.payment && (
<div className="bg-indigo-50 rounded-lg p-4">
<h4 className="text-md font-medium text-gray-900 mb-3 flex items-center gap-2">
<Money size={20} color="#312E81" />
اطلاعات پرداخت
</h4>
<div className="text-sm text-gray-700">
<pre className="whitespace-pre-wrap">{String(JSON.stringify(request.payment, null, 2))}</pre>
</div>
</div>
)}
{/* تاریخ ایجاد */}
<div className="flex items-center justify-between text-sm text-gray-600 border-t pt-4">
<span>تاریخ ایجاد:</span>
<div className="flex items-center gap-1">
<Calendar size={16} color="#6B7280" />
<span>{formatDate(request._id ? new Date().toISOString() : '')}</span>
</div>
</div>
<p>در حال حاضر این modal در حال توسعه است.</p>
</div>
</DefaulModal>
)
}
export default ProductRequestDetailModal
export default ProductRequestDetailModal
+49
View File
@@ -1,5 +1,9 @@
import * as api from "../service/SellerService";
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
CreateCategoryLearning,
CreateLearningType,
} from "../types/Types";
export const useGetSellers = (page: number = 1, enabled: boolean = true) => {
return useQuery({
@@ -66,3 +70,48 @@ export const useGetRequestCreateProduct = (page: number = 1) => {
queryFn: () => api.getRequestCreateProduct(page),
});
};
export const useGetLearningCategory = () => {
return useQuery({
queryKey: ["learning-category"],
queryFn: () => api.getLearningCategory(),
});
};
export const useCreateLearningCategory = () => {
return useMutation({
mutationFn: api.createLearningCategory,
});
};
export const useUpdateLearningCategory = () => {
return useMutation({
mutationFn: ({
id,
params,
}: {
id: string;
params: CreateCategoryLearning;
}) => api.updateLearningCategory(id, params),
});
};
export const useGetLearning = () => {
return useQuery({
queryKey: ["learning"],
queryFn: () => api.getLearning(),
});
};
export const useCreateLearning = () => {
return useMutation({
mutationFn: api.createLearning,
});
};
export const useUpdateLearning = () => {
return useMutation({
mutationFn: ({ id, params }: { id: string; params: CreateLearningType }) =>
api.updateLearning(id, params),
});
};
+46
View File
@@ -7,6 +7,9 @@ import type {
NotificationsResponse,
CreateAnnouncementSpecificSeller,
GetRequestCreateProductResponse,
CreateCategoryLearning,
GetLearningCategoryResponse,
CreateLearningType,
} from "../types/Types";
export const getSellers = async (
@@ -87,3 +90,46 @@ export const getRequestCreateProduct = async (
);
return data;
};
export const getLearningCategory = async (): Promise<
GetLearningCategoryResponse
> => {
const { data } = await axios.get(`/learning/category`);
return data;
};
export const createLearningCategory = async (
params: CreateCategoryLearning
) => {
const { data } = await axios.post(`/admin/learning/category`, params);
return data;
};
export const updateLearningCategory = async (
id: string,
params: CreateCategoryLearning
) => {
const { data } = await axios.patch(
`/admin/learning/category/${id}/update`,
params
);
return data;
};
export const getLearning = async () => {
const { data } = await axios.get(`/learning`);
return data;
};
export const createLearning = async (params: CreateLearningType) => {
const { data } = await axios.post(`/admin/learning`, params);
return data;
};
export const updateLearning = async (
id: string,
params: CreateLearningType
) => {
const { data } = await axios.patch(`/admin/learning/${id}/update`, params);
return data;
};
+24
View File
@@ -217,6 +217,22 @@ export interface CreateCategoryLearning {
title: string;
}
export interface LearningCategory {
_id: string;
title: string;
deleted: boolean;
createdAt: string;
updatedAt: string;
}
export interface GetLearningCategoryResponse {
status: number;
success: boolean;
results: {
learningCategory: LearningCategory[];
};
}
// Product Request Types
export interface City {
_id: number;
@@ -318,3 +334,11 @@ export interface GetRequestCreateProductResponse {
pager: Pager;
};
}
export interface CreateLearningType {
title: string;
description: string;
videoUrl: string;
cover: string;
category: string;
}