This commit is contained in:
hamid zarghami
2025-08-21 12:37:02 +03:30
parent ae0deb7cbe
commit fffe4112c6
9 changed files with 311 additions and 2 deletions
+3
View File
@@ -43,6 +43,9 @@ export const Pages = {
list: "/criticisms/list", list: "/criticisms/list",
detail: "/ccriticisms/detail/", detail: "/ccriticisms/detail/",
}, },
feedback: {
list: "/feedback/list",
},
learning: { learning: {
list: "/learning/list", list: "/learning/list",
detail: "/learning/detail/", detail: "/learning/detail/",
+27
View File
@@ -0,0 +1,27 @@
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;
}
};
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;
}
};
+2 -1
View File
@@ -71,7 +71,8 @@
"card": "حساب های بانکی", "card": "حساب های بانکی",
"payments": "پرداخت ها", "payments": "پرداخت ها",
"sliders": "اسلایدر", "sliders": "اسلایدر",
"support": "پلن پشتیبانی" "support": "پلن پشتیبانی",
"feedback": "گزارش ها"
}, },
"slider": { "slider": {
"new_slider": "اسلایدر جدید", "new_slider": "اسلایدر جدید",
+163
View File
@@ -0,0 +1,163 @@
import { FC, useState } from 'react'
import { useGetFeedbacks } from './hooks/useFeedBackData'
import { Feedback } from './types/FeedbackTypes'
import PageLoading from '../../components/PageLoading'
import Pagination from '../../components/Pagination'
import Td from '../../components/Td'
import { formatDate } from '../../helpers/func'
import { Eye } from 'iconsax-react'
import DefaulModal from '../../components/DefaulModal'
const List: FC = () => {
const [page, setPage] = useState<number>(1)
const [selectedFeedback, setSelectedFeedback] = useState<Feedback | null>(null)
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
const { data, isLoading } = useGetFeedbacks(page)
const handleViewFeedback = (feedback: Feedback) => {
setSelectedFeedback(feedback)
setIsModalOpen(true)
}
const closeModal = () => {
setIsModalOpen(false)
setSelectedFeedback(null)
}
if (isLoading) {
return <PageLoading />
}
const feedbacks = data?.data?.feedbacks || []
const pager = data?.data?.pager
const renderUserInfo = (feedback: Feedback) => {
const contact = feedback.email || feedback.phone || 'نامشخص'
return (
<div className="text-right">
<div className="text-xs text-gray-400">{contact}</div>
</div>
)
}
return (
<div className='mt-4'>
<div className='flex w-full justify-between items-center'>
<div>
لیست فیدبکها
</div>
</div>
{
isLoading ?
<PageLoading />
:
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text="کاربر" />
<Td text="IP" />
<Td text="پلتفرم" />
<Td text="سرویس" />
<Td text="محتوا" />
<Td text="امتیاز" />
<Td text="تاریخ" />
<Td text="" />
</tr>
</thead>
<tbody>
{
feedbacks.map((feedback: Feedback) => {
return (
<tr key={feedback.id} className='tr'>
<Td text="">
{renderUserInfo(feedback)}
</Td>
<Td text="">
<div className="text-xs text-gray-500 font-mono">
{feedback.metadata.userIp}
</div>
</Td>
<Td text={feedback.metadata?.platform ?? ''} />
<Td text="">
<div className="text-right">
<div className="font-medium text-gray-900">
{feedback.service.name}
</div>
<div className="text-sm text-gray-500">
{feedback.service.title}
</div>
</div>
</Td>
<Td text="">
<div className="max-w-xs">
<p className="text-sm text-gray-900 line-clamp-3">
{feedback.content}
</p>
</div>
</Td>
<Td text="">
{feedback.rating}
</Td>
<Td text="">
<div className="text-sm text-gray-900">
{formatDate(feedback.createdAt)}
</div>
</Td>
<Td text="">
<span
className='text-[blue] cursor-pointer flex items-center gap-2'
onClick={() => handleViewFeedback(feedback)}
>
<Eye size={20} color='blue' />
<span className='text-xs'>مشاهده</span>
</span>
</Td>
</tr>
)
})
}
</tbody>
</table>
{feedbacks.length === 0 && (
<div className="text-center py-12">
<div className="text-gray-500 text-lg">هیچ فیدبکی یافت نشد</div>
</div>
)}
{pager && pager.totalPages > 1 && (
<Pagination
totalPages={pager.totalPages}
currentPage={page}
onPageChange={setPage}
/>
)}
</div>
}
{/* مودال نمایش جزئیات فیدبک */}
<DefaulModal
open={isModalOpen}
close={closeModal}
isHeader={true}
title_header="جزئیات فیدبک"
width={600}
>
{selectedFeedback && (
<div className="space-y-6 mt-6">
{selectedFeedback.content}
</div>
)}
</DefaulModal>
</div>
)
}
export default List
@@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { FeedbackResponse } from "../types/FeedbackTypes";
export const useGetFeedbacks = (page: number) => {
return useQuery<FeedbackResponse>({
queryKey: ["feedbacks", page],
queryFn: () => api.getFeedbacks(page),
});
};
+6
View File
@@ -0,0 +1,6 @@
import axios from "../../../config/axios";
export const getFeedbacks = async (page: number) => {
const { data } = await axios.get(`/danak-services/feedback?page=${page}`);
return data;
};
+89
View File
@@ -0,0 +1,89 @@
export interface FeedbackUser {
id: string;
createdAt: string;
updatedAt: string;
email: string | null;
phone: string;
userName: string;
firstName: string;
lastName: string;
birthDate: string;
nationalCode: string;
profilePic: string | null;
emailVerified: boolean;
financialType: string | null;
deletedAt: string | null;
}
export interface FeedbackService {
id: string;
createdAt: string;
updatedAt: string;
name: string;
title: string;
pageTitle: string;
pageDescription: string;
slug: string;
isDanakSuggest: boolean;
description: string;
author: string;
createDate: string;
userCount: number;
serviceLanguage: string;
softwareLanguage: string;
metaDescription: string;
isActive: boolean;
showInSlider: boolean;
link: string;
icon: string;
coverUrl: string;
deletedAt: string | null;
}
export interface FeedbackMetadata {
userIp: string;
referer: string;
userAgent: string;
acceptLanguage: string;
acceptEncoding: string;
contentType: string;
origin: string;
host: string;
xForwardedFor: string;
xRealIp: string;
timestamp: string;
platform?: string;
mobile?: string;
}
export interface Feedback {
id: string;
createdAt: string;
updatedAt: string;
content: string;
rating: number;
metadata: FeedbackMetadata;
user: FeedbackUser;
phone: string | null;
email: string | null;
service: FeedbackService;
deletedAt: string | null;
}
export interface FeedbackPager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
}
export interface FeedbackResponse {
statusCode: number;
success: boolean;
data: {
pager: FeedbackPager;
feedbacks: Feedback[];
};
}
+2
View File
@@ -72,6 +72,7 @@ import PlanUsers from '../pages/support/PlanUsers'
import List from '../pages/guide/List' import List from '../pages/guide/List'
import CreateGuide from '../pages/guide/Create' import CreateGuide from '../pages/guide/Create'
import UpdateGuide from '../pages/guide/Update.tsx' import UpdateGuide from '../pages/guide/Update.tsx'
import ListFeedback from '../pages/feedback/List'
const MainRouter: FC = () => { const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore() const { hasSubMenu } = useSharedStore()
@@ -155,6 +156,7 @@ const MainRouter: FC = () => {
<Route path={Pages.services.guide + ':id'} element={<List />} /> <Route path={Pages.services.guide + ':id'} element={<List />} />
<Route path={Pages.services.guide + ':id/create'} element={<CreateGuide />} /> <Route path={Pages.services.guide + ':id/create'} element={<CreateGuide />} />
<Route path={Pages.services.guide + ':id/:guideId'} element={<UpdateGuide />} /> <Route path={Pages.services.guide + ':id/:guideId'} element={<UpdateGuide />} />
<Route path={Pages.feedback.list} element={<ListFeedback />} />
</Routes> </Routes>
</div> </div>
</div> </div>
+9 -1
View File
@@ -2,7 +2,7 @@ import { FC, useEffect } from 'react'
import LogoImage from '../assets/images/logo.svg' import LogoImage from '../assets/images/logo.svg'
import LogoSmall from '../assets/images/logo-small.svg' import LogoSmall from '../assets/images/logo-small.svg'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react' import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Message, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react'
import SideBarItem from './SideBarItem' import SideBarItem from './SideBarItem'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Pages } from '../config/Pages' import { Pages } from '../config/Pages'
@@ -176,6 +176,14 @@ const SideBar: FC = () => {
activeName='blogs' activeName='blogs'
/> />
<SideBarItem
icon={<Message variant={isActive('feedback') ? 'Bold' : 'Outline'} color={isActive('feedback') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.feedback')}
isActive={isActive('feedback')}
link={Pages.feedback.list}
activeName='services'
/>
</div> </div>
{ {