product report list + detail report
This commit is contained in:
@@ -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 { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const useGetPayments = (page: number = 1) => {
|
||||
return useQuery({
|
||||
@@ -7,3 +7,16 @@ export const useGetPayments = (page: number = 1) => {
|
||||
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 type { PaymentResponse } from "../types/Types";
|
||||
import type { CreateFineType, PaymentResponse } from "../types/Types";
|
||||
|
||||
export const getPayments = async (
|
||||
page: number = 1
|
||||
@@ -7,3 +7,13 @@ export const getPayments = async (
|
||||
const { data } = await axios.get(`/admin/financial/payments?page=${page}`);
|
||||
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;
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user