From 9ec483383e547b0a84428d8d6f2b2087a6c655b6 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 29 Sep 2025 16:04:57 +0330 Subject: [PATCH] product report list + detail report --- src/components/DefaulModal.tsx | 14 +- src/config/Pages.ts | 5 + src/pages/payment/CreateFine.tsx | 87 ++++++ src/pages/payment/Fines.tsx | 10 + src/pages/payment/hooks/usePaymentData.ts | 15 +- src/pages/payment/service/PaymentService.ts | 12 +- src/pages/payment/types/Types.ts | 5 + src/pages/report/ProductReport.tsx | 301 ++++++++++++++++++++ src/pages/report/hooks/useReportData.ts | 9 + src/pages/report/service/ReportService.ts | 7 + src/pages/report/types/Types.ts | 83 ++++++ src/pages/tickets/List.tsx | 12 + src/pages/tickets/hooks/useTicketData.ts | 9 + src/pages/tickets/service/TicketService.ts | 6 + src/router/Main.tsx | 10 + src/shared/SideBar.tsx | 5 + 16 files changed, 580 insertions(+), 10 deletions(-) create mode 100644 src/pages/payment/CreateFine.tsx create mode 100644 src/pages/payment/Fines.tsx create mode 100644 src/pages/report/ProductReport.tsx create mode 100644 src/pages/report/hooks/useReportData.ts create mode 100644 src/pages/report/service/ReportService.ts create mode 100644 src/pages/report/types/Types.ts create mode 100644 src/pages/tickets/List.tsx create mode 100644 src/pages/tickets/hooks/useTicketData.ts create mode 100644 src/pages/tickets/service/TicketService.ts diff --git a/src/components/DefaulModal.tsx b/src/components/DefaulModal.tsx index f321697..1af8dae 100644 --- a/src/components/DefaulModal.tsx +++ b/src/components/DefaulModal.tsx @@ -26,11 +26,13 @@ const DefaulModal: FC = (props: Props) => { { props.open && ( -
-
-
- + {/* Backdrop */} +
+ {/* Modal Container */} +
+
+
{ props.isHeader && props.title_header &&
@@ -40,13 +42,9 @@ const DefaulModal: FC = (props: Props) => { } {props.children} - -
- -
) } diff --git a/src/config/Pages.ts b/src/config/Pages.ts index dd6daa1..b1cdf4b 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -113,6 +113,10 @@ export const Pages = { payment: { list: "/payments/list", }, + report: { + product: "/reports/products", + category: "/reports/category", + }, support: { create: "/support/create", list: "/support/list", @@ -164,5 +168,6 @@ export const Pages = { financial: { list: "/financial/transactions", fines: "/financial/fines", + createFine: "/financial/create-fine", }, }; diff --git a/src/pages/payment/CreateFine.tsx b/src/pages/payment/CreateFine.tsx new file mode 100644 index 0000000..691bd66 --- /dev/null +++ b/src/pages/payment/CreateFine.tsx @@ -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({ + 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 ( +
+
+
+

ساخت قانون جریمه جدید

+
+
+ +
+
+ +
+
+

اطلاعات قانون جریمه

+ +
+ + + +
+
+
+
+ ) +} + +export default CreateFine \ No newline at end of file diff --git a/src/pages/payment/Fines.tsx b/src/pages/payment/Fines.tsx new file mode 100644 index 0000000..ce96690 --- /dev/null +++ b/src/pages/payment/Fines.tsx @@ -0,0 +1,10 @@ +import { type FC } from 'react' + +const Fines: FC = () => { + + return ( +
Fines
+ ) +} + +export default Fines \ No newline at end of file diff --git a/src/pages/payment/hooks/usePaymentData.ts b/src/pages/payment/hooks/usePaymentData.ts index 9377515..726c275 100644 --- a/src/pages/payment/hooks/usePaymentData.ts +++ b/src/pages/payment/hooks/usePaymentData.ts @@ -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, + }); +}; diff --git a/src/pages/payment/service/PaymentService.ts b/src/pages/payment/service/PaymentService.ts index e0b4531..a7e7ed4 100644 --- a/src/pages/payment/service/PaymentService.ts +++ b/src/pages/payment/service/PaymentService.ts @@ -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; +}; diff --git a/src/pages/payment/types/Types.ts b/src/pages/payment/types/Types.ts index c558ef6..b4feb4c 100644 --- a/src/pages/payment/types/Types.ts +++ b/src/pages/payment/types/Types.ts @@ -49,3 +49,8 @@ export interface Pager { prevPage: boolean | null; nextPage: string | null; } + +export type CreateFineType = { + title: string; + fine_percentage: number; +}; diff --git a/src/pages/report/ProductReport.tsx b/src/pages/report/ProductReport.tsx new file mode 100644 index 0000000..67e11a6 --- /dev/null +++ b/src/pages/report/ProductReport.tsx @@ -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(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = (report: Report) => { + setSelectedReport(report); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedReport(null); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ +
+ ); + } + + 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 ( +
+
+ + + + + + + {reports.length === 0 ? ( + + + + ) : ( + reports.map((report: Report) => ( + + + + + + + + + )) + )} + +
+ + + + + + +
+ هیچ گزارشی یافت نشد +
+
+
+ {report.user.fullName} +
+
+ {report.user.phoneNumber} +
+
+
+
+
+ {report.product.title_fa} +
+
+ {report.product.title_en} +
+
+
+
+ {report.reason.slice(0, 2).map((reason) => ( + + {reason.title} + + ))} + {report.reason.length > 2 && ( + + +{report.reason.length - 2} دلیل دیگر + + )} +
+
+
+ {report.description} +
+
+ + + + +
+
+ + + {selectedReport && ( +
+ {/* اطلاعات گزارش‌دهنده */} +
+

گزارش‌دهنده

+
+
+ +

{selectedReport.user.fullName}

+
+
+ +

{selectedReport.user.phoneNumber}

+
+ {selectedReport.user.nationalCode && ( +
+ +

{selectedReport.user.nationalCode}

+
+ )} + {selectedReport.user.address && ( +
+ +

{selectedReport.user.address}

+
+ )} +
+
+ + {/* اطلاعات محصول */} +
+

محصول گزارش‌شده

+
+
+ +

{selectedReport.product.title_fa}

+
+
+ +

{selectedReport.product.title_en}

+
+
+ +

{selectedReport.product.model}

+
+
+ +

{selectedReport.product.brand}

+
+ {selectedReport.product.imagesUrl?.cover && ( +
+ + {selectedReport.product.title_fa} +
+ )} +
+
+ + {/* دلایل گزارش */} +
+

دلایل گزارش

+
+ {selectedReport.reason.map((reason) => ( + + {reason.title} + + ))} +
+ {selectedReport.reason.some(r => r.placeholder) && ( +
+ +
+ {selectedReport.reason.filter(r => r.placeholder).map((reason) => ( +

+ {reason.title}: {reason.placeholder} +

+ ))} +
+
+ )} +
+ + {/* توضیحات گزارش */} +
+

توضیحات گزارش

+

{selectedReport.description}

+
+ + {/* وضعیت و تاریخ */} +
+

اطلاعات گزارش

+
+
+ + +
+
+ +

{formatDate(selectedReport.createdAt)}

+
+
+ +

{formatDate(selectedReport.updatedAt)}

+
+
+
+
+ )} +
+
+ ) +} + +export default ProductReport \ No newline at end of file diff --git a/src/pages/report/hooks/useReportData.ts b/src/pages/report/hooks/useReportData.ts new file mode 100644 index 0000000..8218181 --- /dev/null +++ b/src/pages/report/hooks/useReportData.ts @@ -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, + }); +}; diff --git a/src/pages/report/service/ReportService.ts b/src/pages/report/service/ReportService.ts new file mode 100644 index 0000000..045969a --- /dev/null +++ b/src/pages/report/service/ReportService.ts @@ -0,0 +1,7 @@ +import axios from "../../../config/axios"; +import type { ReportResponse } from "../types/Types"; + +export const getProductReport = async (): Promise => { + const { data } = await axios.get("/admin/products/user-reports"); + return data; +}; diff --git a/src/pages/report/types/Types.ts b/src/pages/report/types/Types.ts new file mode 100644 index 0000000..4d73f44 --- /dev/null +++ b/src/pages/report/types/Types.ts @@ -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; +} diff --git a/src/pages/tickets/List.tsx b/src/pages/tickets/List.tsx new file mode 100644 index 0000000..4b6179d --- /dev/null +++ b/src/pages/tickets/List.tsx @@ -0,0 +1,12 @@ +import { type FC } from 'react' + +const TicketsList: FC = () => { + + return ( +
+ Tickets List +
+ ) +} + +export default TicketsList \ No newline at end of file diff --git a/src/pages/tickets/hooks/useTicketData.ts b/src/pages/tickets/hooks/useTicketData.ts new file mode 100644 index 0000000..7eb8451 --- /dev/null +++ b/src/pages/tickets/hooks/useTicketData.ts @@ -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, + }); +}; diff --git a/src/pages/tickets/service/TicketService.ts b/src/pages/tickets/service/TicketService.ts new file mode 100644 index 0000000..beff5bc --- /dev/null +++ b/src/pages/tickets/service/TicketService.ts @@ -0,0 +1,6 @@ +import axios from "../../../config/axios"; + +export const getTickets = async () => { + const { data } = await axios.get(`/admin/tickets`); + return data; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 7b3934e..88325cc 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -39,6 +39,10 @@ import UpdateBrand from '@/pages/brand/Update' import UpdateWarranty from '@/pages/warranty/Update' import PaymentList from '@/pages/payment/List' 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 { hasSubMenu } = useSharedStore() @@ -99,6 +103,12 @@ const MainRouter: FC = () => { } /> } /> + } /> + } /> + + } /> + + } />
diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index 24ff352..b8322c6 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -667,6 +667,11 @@ const ReportsSubMenu: FC = () => { isActive={isActive('products')} link="/reports/products" /> +
)