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 (
+
+ )
+ }
+ },
+ {
+ 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')}