From f995d03b30b292dd9a19a68146b930d45f373900 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 9 Dec 2025 11:52:47 +0330 Subject: [PATCH] list of reviews --- src/config/Pages.ts | 4 + src/helpers/func.ts | 36 +++--- src/langs/fa.json | 13 ++- src/pages/review/List.tsx | 58 ++++++++++ .../review/components/ReviewFiltersFields.tsx | 12 ++ .../review/components/ReviewTableColumns.tsx | 104 ++++++++++++++++++ src/pages/review/hooks/useReviewData.ts | 9 ++ src/pages/review/hooks/useReviewFilters.ts | 50 +++++++++ src/pages/review/service/ReviewService.ts | 11 ++ src/pages/review/types/Types.ts | 67 +++++++++++ src/router/Main.tsx | 3 + src/shared/SideBar.tsx | 11 +- 12 files changed, 354 insertions(+), 24 deletions(-) create mode 100644 src/pages/review/List.tsx create mode 100644 src/pages/review/components/ReviewFiltersFields.tsx create mode 100644 src/pages/review/components/ReviewTableColumns.tsx create mode 100644 src/pages/review/hooks/useReviewData.ts create mode 100644 src/pages/review/hooks/useReviewFilters.ts create mode 100644 src/pages/review/service/ReviewService.ts create mode 100644 src/pages/review/types/Types.ts diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 7e8166d..2130395 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -71,4 +71,8 @@ export const Pages = { add: "/coupons/add", update: "/coupons/update/", }, + reviews: { + list: "/reviews/list", + detail: "/reviews/detail/", + }, }; diff --git a/src/helpers/func.ts b/src/helpers/func.ts index d32a934..8dc7502 100644 --- a/src/helpers/func.ts +++ b/src/helpers/func.ts @@ -1,29 +1,21 @@ export const formatDate = (dateString: string): string => { - try { - const date = new Date(dateString); - return new Intl.DateTimeFormat("fa-IR", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }).format(date); - } catch (error) { - return dateString; - } + const date = new Date(dateString); + return new Intl.DateTimeFormat("fa-IR", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); }; export const formatDateShort = (dateString: string): string => { - try { - const date = new Date(dateString); - return new Intl.DateTimeFormat("fa-IR", { - year: "numeric", - month: "short", - day: "numeric", - }).format(date); - } catch (error) { - return dateString; - } + const date = new Date(dateString); + return new Intl.DateTimeFormat("fa-IR", { + year: "numeric", + month: "short", + day: "numeric", + }).format(date); }; export const formatOptionalDate = ( diff --git a/src/langs/fa.json b/src/langs/fa.json index 746d037..a7bd941 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -64,7 +64,8 @@ "roles": "سطح دسترسی ها", "payment_methods": "روش های پرداخت", "admins": "مدیران", - "shipment_methods": "روش های ارسال" + "shipment_methods": "روش های ارسال", + "reviews": "نظرات" }, "shipment": { "dineIn": "سرو در محل", @@ -853,5 +854,15 @@ "readyForDeliveryCar": "آماده برای تحویل با ماشین", "readyForDeliveryCourier": "آماده برای تحویل با پیک", "delivered": "تحویل داده شده" + }, + "reviews": { + "reviews": "نظرات", + "reviews_list": "لیست نظرات", + "add_review": "افزودن نظر", + "edit_review": "ویرایش نظر", + "review_list": "لیست نظرات", + "review_name": "نام نظر", + "review_email": "ایمیل نظر", + "review_comment": "نظر" } } diff --git a/src/pages/review/List.tsx b/src/pages/review/List.tsx new file mode 100644 index 0000000..db512c1 --- /dev/null +++ b/src/pages/review/List.tsx @@ -0,0 +1,58 @@ +import { type FC } from 'react' +import Table from '@/components/Table' +import Filters from '@/components/Filters' +import { useGetReviews } from './hooks/useReviewData' +import { useReviewFilters } from './hooks/useReviewFilters' +import { getReviewTableColumns } from './components/ReviewTableColumns' +import { useReviewFiltersFields } from './components/ReviewFiltersFields' +import type { Review } from './types/Types' +import type { RowDataType } from '@/components/types/TableTypes' + +const ReviewsList: FC = () => { + const { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + } = useReviewFilters() + + const { data: reviewsData, isLoading } = useGetReviews(apiParams) + + const reviews = reviewsData?.data || [] + const columns = getReviewTableColumns() + const filterFields = useReviewFiltersFields() + + const totalPages = Math.ceil(reviews.length / limit) || 1 + + return ( +
+
+

لیست نظرات

+
+ +
+ +
+ + + columns={columns} + data={reviews as (Review & RowDataType)[]} + isloading={isLoading} + pagination={{ + currentPage, + totalPages, + onPageChange: handlePageChange, + }} + /> +
+ ) +} + +export default ReviewsList \ No newline at end of file diff --git a/src/pages/review/components/ReviewFiltersFields.tsx b/src/pages/review/components/ReviewFiltersFields.tsx new file mode 100644 index 0000000..4c2119d --- /dev/null +++ b/src/pages/review/components/ReviewFiltersFields.tsx @@ -0,0 +1,12 @@ +import { useMemo } from 'react' +import type { FieldType } from '@/components/Filters' + +export const useReviewFiltersFields = (): FieldType[] => { + return useMemo(() => [ + { + type: 'input', + name: 'search', + placeholder: 'جستجو (نام کاربر یا غذا)', + }, + ], []) +} diff --git a/src/pages/review/components/ReviewTableColumns.tsx b/src/pages/review/components/ReviewTableColumns.tsx new file mode 100644 index 0000000..ec45d45 --- /dev/null +++ b/src/pages/review/components/ReviewTableColumns.tsx @@ -0,0 +1,104 @@ +import { Eye, Star1 } from 'iconsax-react' +import type { ColumnType } from '@/components/types/TableTypes' +import type { Review } from '../types/Types' +import { formatOptionalDate } from '@/helpers/func' +import Status from '@/components/Status' +import { Link } from 'react-router-dom' +import { Pages } from '@/config/Pages' + +const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => { + const variantMap: Record = { + 'pending': 'pending', + 'approved': 'success', + 'rejected': 'error', + } + return variantMap[status] || 'pending' +} + +export const getReviewTableColumns = (): ColumnType[] => { + return [ + { + key: 'user', + title: 'کاربر', + render: (item: Review) => { + const name = [item.user.firstName, item.user.lastName].filter(Boolean).join(' ') + return name || '-' + } + }, + { + key: 'food', + title: 'غذا', + render: (item: Review) => { + return item.food.title || '-' + } + }, + { + key: 'rating', + title: 'امتیاز', + render: (item: Review) => { + return ( +
+ +
{item.rating}
+
+ ) + } + }, + { + key: 'comment', + title: 'نظر', + render: (item: Review) => { + return ( +
+ {item.comment || '-'} +
+ ) + } + }, + { + key: 'status', + title: 'وضعیت', + render: (item: Review) => { + const statusLabels: Record = { + 'pending': 'در انتظار', + 'approved': 'تایید شده', + 'rejected': 'رد شده', + } + return ( + + ) + } + }, + { + key: 'createdAt', + title: 'تاریخ ثبت', + render: (item: Review) => { + return formatOptionalDate(item.createdAt, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } + }, + { + key: 'details', + title: '', + render: (item: Review) => { + return ( + + + + ) + } + }, + ] +} diff --git a/src/pages/review/hooks/useReviewData.ts b/src/pages/review/hooks/useReviewData.ts new file mode 100644 index 0000000..72d1054 --- /dev/null +++ b/src/pages/review/hooks/useReviewData.ts @@ -0,0 +1,9 @@ +import * as api from "../service/ReviewService"; +import { useQuery } from "@tanstack/react-query"; + +export const useGetReviews = (params?: Record) => { + return useQuery({ + queryKey: ["reviews", params], + queryFn: () => api.getReviews(params), + }); +}; diff --git a/src/pages/review/hooks/useReviewFilters.ts b/src/pages/review/hooks/useReviewFilters.ts new file mode 100644 index 0000000..d420d8f --- /dev/null +++ b/src/pages/review/hooks/useReviewFilters.ts @@ -0,0 +1,50 @@ +import { useState, useMemo } from "react"; +import type { FilterValues } from "@/components/Filters"; + +const DEFAULT_PAGE = 1; +const DEFAULT_LIMIT = 10; + +export const useReviewFilters = () => { + const [filters, setFilters] = useState({}); + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE); + const [limit] = useState(DEFAULT_LIMIT); + + const apiParams = useMemo(() => { + const params: Record = { + page: currentPage, + limit: limit, + }; + + if (filters.search) { + params.search = filters.search as string; + } + + if (filters.status) { + params.status = filters.status as string; + } + + if (filters.rating) { + params.rating = filters.rating as string; + } + + return params; + }, [filters, currentPage, limit]); + + const handleFiltersChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(DEFAULT_PAGE); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + return { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + }; +}; diff --git a/src/pages/review/service/ReviewService.ts b/src/pages/review/service/ReviewService.ts new file mode 100644 index 0000000..46a51b3 --- /dev/null +++ b/src/pages/review/service/ReviewService.ts @@ -0,0 +1,11 @@ +import axios from "@/config/axios"; +import type { GetReviewsResponse } from "../types/Types"; + +export const getReviews = async ( + params?: Record +): Promise => { + const { data } = await axios.get("/admin/reviews", { + params, + }); + return data; +}; diff --git a/src/pages/review/types/Types.ts b/src/pages/review/types/Types.ts new file mode 100644 index 0000000..c909ad4 --- /dev/null +++ b/src/pages/review/types/Types.ts @@ -0,0 +1,67 @@ +import type { IResponse } from "@/types/response.types"; +import type { RowDataType } from "@/components/types/TableTypes"; + +export interface ReviewFood { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + restaurant: string; + category: string; + title: string; + desc: string; + content: string | null; + price: number; + points: number | null; + order: number | null; + prepareTime: number | null; + sat: boolean; + sun: boolean; + mon: boolean; + breakfast: boolean; + noon: boolean; + dinner: boolean; + stock: number; + stockDefault: number; + isActive: boolean; + images: string[]; + inPlaceServe: boolean; + pickupServe: boolean; + rate: number; + discount: number; + isSpecialOffer: boolean; +} + +export interface ReviewUser { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + firstName: string; + lastName: string; + birthDate: string; + marriageDate: string; + referrer: string | null; + isActive: boolean; + gender: boolean; + wallet: number; + points: number; + phone: string; +} + +export interface Review extends RowDataType { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + order: string; + food: ReviewFood; + user: ReviewUser; + comment: string; + rating: number; + positivePoints: string[]; + negativePoints: string[]; + status: string; +} + +export type GetReviewsResponse = IResponse; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 88c2207..f0c1db4 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -36,6 +36,7 @@ import UpdateShipmentMethod from '@/pages/shipmentMethod/Update' import CouponsList from '@/pages/coupon/List' import CreateCoupon from '@/pages/coupon/Create' import UpdateCoupon from '@/pages/coupon/Update' +import ReviewsList from '@/pages/review/List' const MainRouter: FC = () => { const { hasSubMenu } = useSharedStore() @@ -87,6 +88,8 @@ const MainRouter: FC = () => { } /> } /> } /> + + } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index faf8a1f..8f58d97 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -2,7 +2,7 @@ import { type FC, useEffect } from 'react' import LogoImage from '../assets/images/logo.svg' import LogoSmall from '../assets/images/logo-small.svg' import { useTranslation } from 'react-i18next' -import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Chart, Calendar, Security, Card, SecurityUser, TruckFast, Element3 } from 'iconsax-react' +import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Chart, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1 } from 'iconsax-react' import SideBarItem from './SideBarItem' import { useLocation } from 'react-router-dom' import { Pages } from '../config/Pages' @@ -120,6 +120,15 @@ const SideBar: FC = () => { activeName='customers' /> + } + title={t('sidebar.reviews')} + isActive={isActive('reviews')} + link={Pages.reviews.list} + name='reviews' + activeName='reviews' + /> + } title={t('sidebar.admins')}