product report list + detail report
This commit is contained in:
@@ -26,11 +26,13 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
{
|
{
|
||||||
props.open && (
|
props.open && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-hidden fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
{/* Backdrop */}
|
||||||
<div className='relative xl:h-full h-[80%] bottom-0 left-0 max-h-[80%] top-0 m-auto overflow-hidden flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 bg-black/50 backdrop-blur-md inset-0 z-50'></div>
|
||||||
<div className='border-0 h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full bg-white outline-none focus:outline-none'>
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Modal Container */}
|
||||||
|
<div className='fixed inset-0 z-[60] flex xl:justify-center xl:items-start items-end overflow-x-hidden overflow-y-auto'>
|
||||||
|
<div style={{ maxWidth: props.width }} className='relative xl:max-h-[90vh] xl:my-6 xl:max-w-xl mx-auto w-full xl:w-auto'>
|
||||||
|
<div className='border-0 xl:h-auto max-h-[80vh] p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full bg-white outline-none focus:outline-none'>
|
||||||
{
|
{
|
||||||
props.isHeader && props.title_header &&
|
props.isHeader && props.title_header &&
|
||||||
<div className='pb-6 border-b border-white/20'>
|
<div className='pb-6 border-b border-white/20'>
|
||||||
@@ -40,13 +42,9 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 bg-black/50 backdrop-blur-md inset-0 z-50 '></div>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ export const Pages = {
|
|||||||
payment: {
|
payment: {
|
||||||
list: "/payments/list",
|
list: "/payments/list",
|
||||||
},
|
},
|
||||||
|
report: {
|
||||||
|
product: "/reports/products",
|
||||||
|
category: "/reports/category",
|
||||||
|
},
|
||||||
support: {
|
support: {
|
||||||
create: "/support/create",
|
create: "/support/create",
|
||||||
list: "/support/list",
|
list: "/support/list",
|
||||||
@@ -164,5 +168,6 @@ export const Pages = {
|
|||||||
financial: {
|
financial: {
|
||||||
list: "/financial/transactions",
|
list: "/financial/transactions",
|
||||||
fines: "/financial/fines",
|
fines: "/financial/fines",
|
||||||
|
createFine: "/financial/create-fine",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import type { CreateFineType } from './types/Types'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useCreateFine } from './hooks/usePaymentData'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import Input from '../../components/Input'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
|
|
||||||
|
const CreateFine: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const createFineMutation = useCreateFine()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateFineType>({
|
||||||
|
initialValues: {
|
||||||
|
fine_percentage: 0,
|
||||||
|
title: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
fine_percentage: Yup.number().required('درصد جریمه الزامی است').min(0, 'درصد جریمه نمیتواند منفی باشد'),
|
||||||
|
title: Yup.string().required('عنوان جریمه الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
createFineMutation.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('قانون جریمه با موفقیت ایجاد شد')
|
||||||
|
navigate(Pages.financial.fines)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<h1>ساخت قانون جریمه جدید</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className='px-6'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createFineMutation.isPending}
|
||||||
|
disabled={!formik.isValid || createFineMutation.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TickCircle size={16} color='#fff' />
|
||||||
|
<div>ثبت قانون جریمه</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-6 xl:mt-8 mt-6'>
|
||||||
|
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
|
||||||
|
<h2 className='text-lg font-medium mb-6'>اطلاعات قانون جریمه</h2>
|
||||||
|
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<Input
|
||||||
|
label='عنوان جریمه'
|
||||||
|
placeholder='مثال: جریمه تأخیر پرداخت'
|
||||||
|
{...formik.getFieldProps('title')}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='درصد جریمه'
|
||||||
|
type='number'
|
||||||
|
placeholder='مثال: 5'
|
||||||
|
{...formik.getFieldProps('fine_percentage')}
|
||||||
|
error_text={formik.touched.fine_percentage && formik.errors.fine_percentage ? formik.errors.fine_percentage : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateFine
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
const Fines: FC = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>Fines</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Fines
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as api from "../service/PaymentService";
|
import * as api from "../service/PaymentService";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const useGetPayments = (page: number = 1) => {
|
export const useGetPayments = (page: number = 1) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -7,3 +7,16 @@ export const useGetPayments = (page: number = 1) => {
|
|||||||
queryFn: () => api.getPayments(page),
|
queryFn: () => api.getPayments(page),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateFine = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createFine,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetFines = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["fines"],
|
||||||
|
queryFn: api.getFines,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from "../../../config/axios";
|
import axios from "../../../config/axios";
|
||||||
import type { PaymentResponse } from "../types/Types";
|
import type { CreateFineType, PaymentResponse } from "../types/Types";
|
||||||
|
|
||||||
export const getPayments = async (
|
export const getPayments = async (
|
||||||
page: number = 1
|
page: number = 1
|
||||||
@@ -7,3 +7,13 @@ export const getPayments = async (
|
|||||||
const { data } = await axios.get(`/admin/financial/payments?page=${page}`);
|
const { data } = await axios.get(`/admin/financial/payments?page=${page}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createFine = async (params: CreateFineType) => {
|
||||||
|
const { data } = await axios.post(`/admin/fine/rule`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFines = async () => {
|
||||||
|
const { data } = await axios.get(`/admin/fine/fines`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -49,3 +49,8 @@ export interface Pager {
|
|||||||
prevPage: boolean | null;
|
prevPage: boolean | null;
|
||||||
nextPage: string | null;
|
nextPage: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateFineType = {
|
||||||
|
title: string;
|
||||||
|
fine_percentage: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import { useGetProductReport } from './hooks/useReportData'
|
||||||
|
import { type Report } from './types/Types'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import Error from '../../components/Error'
|
||||||
|
import Td from '../../components/Td'
|
||||||
|
import StatusWithText from '../../components/StatusWithText'
|
||||||
|
import DefaulModal from '../../components/DefaulModal'
|
||||||
|
import { Eye } from 'iconsax-react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
|
||||||
|
const ProductReport: FC = () => {
|
||||||
|
|
||||||
|
const { data, isLoading, error } = useGetProductReport();
|
||||||
|
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const openModal = (report: Report) => {
|
||||||
|
setSelectedReport(report);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setSelectedReport(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 reports = data?.results?.reports || [];
|
||||||
|
|
||||||
|
const getStatusVariant = (status: string) => {
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case 'pending':
|
||||||
|
return 'warning';
|
||||||
|
case 'resolved':
|
||||||
|
return 'success';
|
||||||
|
case 'rejected':
|
||||||
|
return 'error';
|
||||||
|
default:
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case 'pending':
|
||||||
|
return 'در انتظار بررسی';
|
||||||
|
case 'resolved':
|
||||||
|
return 'بررسی شده';
|
||||||
|
case 'rejected':
|
||||||
|
return 'رد شده';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('fa-IR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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={'وضعیت'} />
|
||||||
|
<Td text={'تاریخ گزارش'} />
|
||||||
|
<Td text={'عملیات'} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{reports.length === 0 ? (
|
||||||
|
<tr className='tr'>
|
||||||
|
<td colSpan={7} className="text-center py-8 text-gray-500">
|
||||||
|
هیچ گزارشی یافت نشد
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
reports.map((report: Report) => (
|
||||||
|
<tr key={report._id} className='tr'>
|
||||||
|
<Td text="">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{report.user.fullName}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 text-xs">
|
||||||
|
{report.user.phoneNumber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{report.product.title_fa}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 text-xs">
|
||||||
|
{report.product.title_en}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{report.reason.slice(0, 2).map((reason) => (
|
||||||
|
<span
|
||||||
|
key={reason._id}
|
||||||
|
className="inline-block bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
{reason.title}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{report.reason.length > 2 && (
|
||||||
|
<span className="inline-block bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">
|
||||||
|
+{report.reason.length - 2} دلیل دیگر
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs truncate" title={report.description}>
|
||||||
|
{report.description}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<StatusWithText
|
||||||
|
variant={getStatusVariant(report.status)}
|
||||||
|
text={getStatusText(report.status)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td text={formatDate(report.createdAt)} />
|
||||||
|
<Td text="">
|
||||||
|
<Button
|
||||||
|
onClick={() => openModal(report)}
|
||||||
|
className="h-8 text-xs w-fit"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Eye color='#000' size={16} className="ml-1" />
|
||||||
|
جزئیات
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DefaulModal
|
||||||
|
open={isModalOpen}
|
||||||
|
close={closeModal}
|
||||||
|
isHeader={true}
|
||||||
|
title_header="جزئیات گزارش محصول"
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{selectedReport && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* اطلاعات گزارشدهنده */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-gray-800">گزارشدهنده</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">نام کامل</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.user.fullName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">شماره تلفن</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.user.phoneNumber}</p>
|
||||||
|
</div>
|
||||||
|
{selectedReport.user.nationalCode && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">کد ملی</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.user.nationalCode}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedReport.user.address && (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-600">آدرس</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.user.address}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* اطلاعات محصول */}
|
||||||
|
<div className="bg-blue-50 p-4 rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-gray-800">محصول گزارششده</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">عنوان فارسی</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.product.title_fa}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">عنوان انگلیسی</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.product.title_en}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">مدل</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.product.model}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">برند</label>
|
||||||
|
<p className="text-gray-900">{selectedReport.product.brand}</p>
|
||||||
|
</div>
|
||||||
|
{selectedReport.product.imagesUrl?.cover && (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-2">تصویر محصول</label>
|
||||||
|
<img
|
||||||
|
src={selectedReport.product.imagesUrl.cover}
|
||||||
|
alt={selectedReport.product.title_fa}
|
||||||
|
className="w-24 h-24 object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* دلایل گزارش */}
|
||||||
|
<div className="bg-red-50 p-4 rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-gray-800">دلایل گزارش</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedReport.reason.map((reason) => (
|
||||||
|
<span
|
||||||
|
key={reason._id}
|
||||||
|
className="inline-block bg-red-100 text-red-800 text-sm px-3 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
{reason.title}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{selectedReport.reason.some(r => r.placeholder) && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-2">توضیحات دلایل</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedReport.reason.filter(r => r.placeholder).map((reason) => (
|
||||||
|
<p key={reason._id} className="text-sm text-gray-700 bg-white p-2 rounded">
|
||||||
|
<strong>{reason.title}:</strong> {reason.placeholder}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* توضیحات گزارش */}
|
||||||
|
<div className="bg-yellow-50 p-4 rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-gray-800">توضیحات گزارش</h3>
|
||||||
|
<p className="text-gray-900 whitespace-pre-wrap">{selectedReport.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* وضعیت و تاریخ */}
|
||||||
|
<div className="bg-green-50 p-4 rounded-xl">
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-gray-800">اطلاعات گزارش</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">وضعیت</label>
|
||||||
|
<StatusWithText
|
||||||
|
variant={getStatusVariant(selectedReport.status)}
|
||||||
|
text={getStatusText(selectedReport.status)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">تاریخ گزارش</label>
|
||||||
|
<p className="text-gray-900">{formatDate(selectedReport.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600">تاریخ آخرین بروزرسانی</label>
|
||||||
|
<p className="text-gray-900">{formatDate(selectedReport.updatedAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductReport
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/ReportService";
|
||||||
|
|
||||||
|
export const useGetProductReport = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["productReport"],
|
||||||
|
queryFn: api.getProductReport,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import type { ReportResponse } from "../types/Types";
|
||||||
|
|
||||||
|
export const getProductReport = async (): Promise<ReportResponse> => {
|
||||||
|
const { data } = await axios.get("/admin/products/user-reports");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
export interface Reason {
|
||||||
|
_id: number;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
placeholder: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
_id: string;
|
||||||
|
fullName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
dateOfBirth: string | null;
|
||||||
|
address: string | null;
|
||||||
|
nationalCode: string | null;
|
||||||
|
homeNumber: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductImagesUrl {
|
||||||
|
cover: string;
|
||||||
|
list: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductSpecification {
|
||||||
|
title: string;
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
seoTitle: string | null;
|
||||||
|
seoDescription: string | null;
|
||||||
|
_id: number;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
model: string;
|
||||||
|
source: string;
|
||||||
|
description: string;
|
||||||
|
metaDescription: string;
|
||||||
|
tags: string[];
|
||||||
|
advantages: string[];
|
||||||
|
disAdvantages: string[];
|
||||||
|
totalRate: number;
|
||||||
|
category: string;
|
||||||
|
shop: string;
|
||||||
|
brand: string;
|
||||||
|
status: string;
|
||||||
|
adminComments: string | null;
|
||||||
|
step: number;
|
||||||
|
isFake: boolean;
|
||||||
|
variants: string[];
|
||||||
|
voice: string | null;
|
||||||
|
deleted: boolean;
|
||||||
|
specifications: ProductSpecification[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
imagesUrl: ProductImagesUrl;
|
||||||
|
url: string;
|
||||||
|
rate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Report {
|
||||||
|
_id: string;
|
||||||
|
user: User;
|
||||||
|
product: Product;
|
||||||
|
reason: Reason[];
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportResults {
|
||||||
|
reports: Report[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: ReportResults;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
const TicketsList: FC = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Tickets List
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TicketsList
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/TicketService";
|
||||||
|
|
||||||
|
export const useGetTickets = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tickets"],
|
||||||
|
queryFn: api.getTickets,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
|
||||||
|
export const getTickets = async () => {
|
||||||
|
const { data } = await axios.get(`/admin/tickets`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -39,6 +39,10 @@ import UpdateBrand from '@/pages/brand/Update'
|
|||||||
import UpdateWarranty from '@/pages/warranty/Update'
|
import UpdateWarranty from '@/pages/warranty/Update'
|
||||||
import PaymentList from '@/pages/payment/List'
|
import PaymentList from '@/pages/payment/List'
|
||||||
import UpdateShipment from '@/pages/shipment/Update'
|
import UpdateShipment from '@/pages/shipment/Update'
|
||||||
|
import CreateFine from '@/pages/payment/CreateFine'
|
||||||
|
import Fines from '@/pages/payment/Fines'
|
||||||
|
import TicketsList from '@/pages/tickets/List'
|
||||||
|
import ProductReport from '@/pages/report/ProductReport'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -99,6 +103,12 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.admin.create} element={<AdminCreate />} />
|
<Route path={Pages.admin.create} element={<AdminCreate />} />
|
||||||
|
|
||||||
<Route path={Pages.financial.list} element={<PaymentList />} />
|
<Route path={Pages.financial.list} element={<PaymentList />} />
|
||||||
|
<Route path={Pages.financial.createFine} element={<CreateFine />} />
|
||||||
|
<Route path={Pages.financial.fines} element={<Fines />} />
|
||||||
|
|
||||||
|
<Route path={Pages.ticket.list} element={<TicketsList />} />
|
||||||
|
|
||||||
|
<Route path={Pages.report.product} element={<ProductReport />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -667,6 +667,11 @@ const ReportsSubMenu: FC = () => {
|
|||||||
isActive={isActive('products')}
|
isActive={isActive('products')}
|
||||||
link="/reports/products"
|
link="/reports/products"
|
||||||
/>
|
/>
|
||||||
|
<SubMenuItem
|
||||||
|
title={'دسته بندی گزارشات'}
|
||||||
|
isActive={isActive('category')}
|
||||||
|
link={Pages.report.category}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user