criticisms

This commit is contained in:
hamid zarghami
2025-10-22 15:37:41 +03:30
parent 8ec11d2711
commit edadb01702
9 changed files with 479 additions and 2 deletions
+4 -1
View File
@@ -27,7 +27,6 @@ export const Paths = {
tickets: {
list: '/tickets',
},
criticisms: '/criticisms',
learning: '/learning',
auth: {
login: '/auth/login',
@@ -43,4 +42,8 @@ export const Paths = {
detail: "/announcement/detail/",
create: "/announcement/create",
},
criticisms: {
list: "/criticisms",
detail: "/criticisms/detail/",
},
}
+96
View File
@@ -0,0 +1,96 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox'
import Button from '../../components/Button'
import { TickSquare } from 'iconsax-react'
const AddCriticisms: FC = () => {
const { t } = useTranslation('global')
return (
<div className='mt-4 text-sm'>
<div>
{t('criticisms.criticisms')}
</div>
<div className='flex gap-6 xl:mt-8 mt-5'>
<div className='flex-1 bg-white py-8 xl:px-10 px-5 rounded-3xl'>
<div>
{t('criticisms.submit_your_criticisms')}
</div>
<div className='mt-3 text-xs text-description'>
{t('criticisms.description_criticisms')}
</div>
<div className='mt-10'>
<Input
label={t('criticisms.subject')}
placeholder={t('criticisms.enter_your_subject')}
/>
</div>
<div className='mt-7'>
<Textarea
label={t('criticisms.description')}
placeholder={t('criticisms.enter_description')}
/>
</div>
<div className='mt-7'>
<UploadBox
label={t('attachment')}
/>
</div>
<div className='mt-20 flex justify-end'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
/>
</div>
</div>
<div className='bg-white w-sidebar hidden xl:block py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default AddCriticisms
+77
View File
@@ -0,0 +1,77 @@
import { type FC, Fragment } from 'react'
import { useGetDetailCriticisms } from './hooks/useCriticismsData'
import { useParams } from 'react-router-dom'
// import { useTranslation } from 'react-i18next'
// import PageLoading from '../../components/PageLoading'
import { Paperclip2 } from 'iconsax-react'
const CriticismsDetail: FC = () => {
const { id } = useParams()
// const { t } = useTranslation('global')
const t = (key: string) => {
const translations: { [key: string]: string } = {
'criticisms.criticism': 'انتقاد',
'criticisms.documents': 'مدارک',
'loading': 'در حال بارگذاری...'
};
return translations[key] || key;
};
const getDetail = useGetDetailCriticisms(id ? id : '')
return (
<div className='mt-4'>
{
getDetail.isPending ?
<div className="flex justify-center items-center py-8">
<div className="text-gray-500">{t('loading')}</div>
</div>
:
<Fragment>
<div>
{t('criticisms.criticism')}
{' #' + getDetail.data?.data?.criticism?.numericId}
</div>
<div className='flex-1 mt-4 bg-white p-8 rounded-2xl'>
<div className='pb-6 border-b'>
<div>
{getDetail.data?.data?.criticism?.title}
</div>
<p className='mt-6 text-sm leading-6'>
{getDetail.data?.data?.criticism?.content}
</p>
</div>
<div className='mt-6 text-sm'>
<div>
{t('criticisms.documents')}
</div>
<div className='mt-4 text-xs flex flex-col gap-4'>
{
getDetail.data?.data?.criticism?.files?.map((item: { id: string, fileUrl: string }) => {
return (
<a href={item.fileUrl} target='_blank' key={item.id} className='flex gap-2 items-center text-[#0047FF]'>
<Paperclip2
className='size-6'
color='#0047FF'
/>
<div>
{item.fileUrl.split('/')[item.fileUrl.split('/').length - 1]}
</div>
</a>
)
})
}
</div>
</div>
</div>
</Fragment>
}
</div>
)
}
export default CriticismsDetail
+139
View File
@@ -0,0 +1,139 @@
import { type FC, useState } from 'react'
import { useDeleteCriticismsDetail, useGetCriticismsList } from './hooks/useCriticismsData'
// import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Eye, Trash } from 'iconsax-react'
import { Paths } from '../../config/Paths'
import type { CriticismsItemTypes } from './types/CriticismsTypes'
import moment from 'moment-jalaali'
import DefaulModal from '../../components/DefaulModal'
import Table from '../../components/Table'
import type { ColumnType } from '../../components/types/TableTypes'
const CriticismsList: FC = () => {
const [itemId, setItemId] = useState<string>('')
const [page, setPage] = useState<number>(1)
const [openConfrim, setOpenConfrim] = useState(false)
const getCriticismsList = useGetCriticismsList(page)
const deleteCriticisms = useDeleteCriticismsDetail()
// const { t } = useTranslation('global')
const t = (key: string) => {
const translations: { [key: string]: string } = {
'criticisms.criticisms_list': 'لیست انتقادات',
'criticisms.number': 'شماره',
'criticisms.title_criticisms': 'عنوان انتقاد',
'criticisms.summary_criticisms': 'خلاصه انتقاد',
'criticisms.date_created': 'تاریخ ایجاد',
'detail': 'جزئیات',
'delete_confirmation': 'تایید حذف',
'are_you_sure_delete': 'آیا از حذف این مورد اطمینان دارید؟',
'cancel': 'لغو',
'delete': 'حذف',
'deleting': 'در حال حذف...'
};
return translations[key] || key;
};
const handleDelete = (id?: string, confrim: boolean = false) => {
if (confrim) {
deleteCriticisms.mutate(itemId, {
onSuccess: () => {
getCriticismsList.refetch()
setOpenConfrim(false)
setItemId('')
}
})
} else if (id) {
setItemId(id)
setOpenConfrim(true)
}
}
// Define table columns
const columns: ColumnType<CriticismsItemTypes>[] = [
{
title: t('criticisms.number'),
key: 'numericId',
},
{
title: t('criticisms.title_criticisms'),
key: 'title',
},
{
title: t('criticisms.summary_criticisms'),
key: 'content',
render: (item) => item.content.substring(0, 100),
},
{
title: t('criticisms.date_created'),
key: 'createdAt',
render: (item) => moment(item.createdAt).format('HH:mm jYYYY-jMM-jDD'),
},
{
title: t('detail'),
key: 'detail',
render: (item) => (
<Link to={Paths.criticisms.detail + item.id}>
<Eye size={20} color='#8C90A3' />
</Link>
),
},
];
return (
<div className='mt-4'>
<div>
{t('criticisms.criticisms_list')}
</div>
<Table
columns={columns}
data={getCriticismsList.data?.data?.criticisms}
isLoading={getCriticismsList.isPending}
rowActions={(item) => [
{
label: t('delete'),
onClick: () => handleDelete(String(item.id)),
icon: <Trash size={20} color='#8C90A3' />,
}
]}
pagination={{
currentPage: page,
totalPages: getCriticismsList.data?.data?.pager?.totalPages || 1,
onPageChange: setPage,
}}
/>
<DefaulModal
open={openConfrim}
close={() => setOpenConfrim(false)}
isHeader={true}
title_header={t('delete_confirmation')}
>
<div className="p-4">
<p className="mb-6">{t('are_you_sure_delete')}</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => setOpenConfrim(false)}
className="px-4 py-2 bg-gray-200 rounded-lg"
>
{t('cancel')}
</button>
<button
onClick={() => handleDelete('', true)}
disabled={deleteCriticisms.isPending}
className="px-4 py-2 bg-red-500 text-white rounded-lg disabled:opacity-50"
>
{deleteCriticisms.isPending ? t('deleting') : t('delete')}
</button>
</div>
</div>
</DefaulModal>
</div>
)
}
export default CriticismsList
@@ -0,0 +1,130 @@
import { useMutation, useQuery } from "@tanstack/react-query";
// import * as api from "../service/CriticismsService";
import type { CriticismsItemTypes } from "../types/CriticismsTypes";
// Static data for development
const staticCriticismsData = {
data: {
criticisms: [
{
id: "1",
numericId: "1001",
title: "مشکل در سیستم پرداخت",
content:
"سلام، من در هنگام پرداخت با مشکل مواجه شدم. سیستم پرداخت به درستی کار نمی‌کند و پس از وارد کردن اطلاعات کارت، خطای عجیبی نمایش داده می‌شود. لطفاً این مشکل را بررسی کنید.",
createdAt: new Date("2024-01-15T10:30:00"),
isRead: false,
files: [
{ id: "f1", fileUrl: "https://example.com/files/payment-error.png" },
{ id: "f2", fileUrl: "https://example.com/files/screenshot.jpg" },
],
},
{
id: "2",
numericId: "1002",
title: "کیفیت محصول مطابق انتظار نبود",
content:
"محصولی که سفارش دادم کیفیت مناسبی نداشت و با توضیحات ارائه شده در سایت مطابقت نداشت. انتظار داشتم کیفیت بهتری داشته باشد.",
createdAt: new Date("2024-01-14T14:20:00"),
isRead: true,
files: [
{ id: "f3", fileUrl: "https://example.com/files/product-photo.jpg" },
],
},
{
id: "3",
numericId: "1003",
title: "تاخیر در ارسال سفارش",
content:
"سفارش من با تاخیر زیادی ارسال شد. زمان تحویل اعلام شده رعایت نشد و این موضوع باعث نارضایتی من شده است.",
createdAt: new Date("2024-01-13T09:15:00"),
isRead: false,
files: [],
},
{
id: "4",
numericId: "1004",
title: "مشکل در پشتیبانی آنلاین",
content:
"سیستم چت آنلاین سایت به درستی کار نمی‌کند و نمی‌توانم با پشتیبانی ارتباط برقرار کنم. لطفاً این مشکل را حل کنید.",
createdAt: new Date("2024-01-12T16:45:00"),
isRead: true,
files: [
{ id: "f4", fileUrl: "https://example.com/files/chat-error.png" },
],
},
{
id: "5",
numericId: "1005",
title: "پیشنهاد بهبود رابط کاربری",
content:
"رابط کاربری سایت نیاز به بهبود دارد. منوها کمی پیچیده هستند و یافتن محصولات مورد نظر دشوار است. پیشنهاد می‌کنم طراحی ساده‌تری استفاده کنید.",
createdAt: new Date("2024-01-11T11:30:00"),
isRead: false,
files: [],
},
] as CriticismsItemTypes[],
pager: {
totalPages: 2,
currentPage: 1,
totalItems: 5,
},
},
};
export const useGetCriticismsList = (page: number) => {
// Commented out API call - using static data instead
// return useQuery({
// queryKey: ["criticisms", page],
// queryFn: () => api.getCriticismsList(page),
// });
return {
data: staticCriticismsData,
isPending: false,
isLoading: false,
refetch: () => Promise.resolve(staticCriticismsData),
};
};
export const useDeleteCriticismsDetail = () => {
// Commented out API call - using mock function instead
// return useMutation({
// mutationFn: (id: string) => api.deleteCriticisms(id),
// });
return {
mutate: (id: string, options?: { onSuccess?: () => void }) => {
// Mock delete - just call onSuccess after a brief delay
setTimeout(() => {
if (options?.onSuccess) {
options.onSuccess();
}
}, 500);
},
isPending: false,
};
};
export const useGetDetailCriticisms = (id: string) => {
// Commented out API call - using static data instead
// return useQuery({
// queryKey: ["criticisms", id],
// queryFn: () => api.getCriticismsDetail(id),
// enabled: !!id,
// });
const criticism = staticCriticismsData.data.criticisms.find(
(item) => item.id === id
);
return {
data: {
data: {
criticism: criticism || null,
},
},
isPending: false,
isLoading: false,
};
};
@@ -0,0 +1,17 @@
// API service functions commented out - using static data instead
// import axios from "../../../config/axios";
// export const getCriticismsList = async (page: number) => {
// const { data } = await axios.get(`/criticisms?page=${page}`);
// return data;
// };
// export const getCriticismsDetail = async (id: string) => {
// const { data } = await axios.get(`/criticisms/${id}`);
// return data;
// };
// export const deleteCriticisms = async (id: string) => {
// const { data } = await axios.delete(`/criticisms/${id}`);
// return data;
// };
@@ -0,0 +1,10 @@
export interface CriticismsItemTypes {
id: string;
content: string;
createdAt: Date;
isRead: boolean;
title: string;
numericId: string;
files?: { id: string; fileUrl: string }[];
[key: string]: unknown;
}
+5
View File
@@ -20,6 +20,8 @@ import TicketCategory from '@/pages/ticket/Category'
import AnnouncementList from '@/pages/annoncement/List'
import AnnouncementUpdate from '@/pages/annoncement/Update'
import AnnouncementCreate from '@/pages/annoncement/Create'
import CriticismsList from '@/pages/criticisms/List'
import CriticismsDetail from '@/pages/criticisms/Detail'
const MainRouter: FC = () => {
return (
<div className='p-4 overflow-hidden'>
@@ -50,6 +52,9 @@ const MainRouter: FC = () => {
<Route path={Paths.announcement.list} element={<AnnouncementList />} />
<Route path={Paths.announcement.detail + ':id'} element={<AnnouncementUpdate />} />
<Route path={Paths.announcement.create} element={<AnnouncementCreate />} />
<Route path={Paths.criticisms.list} element={<CriticismsList />} />
<Route path={Paths.criticisms.detail + ':id'} element={<CriticismsDetail />} />
</Routes>
</div>
</div>
+1 -1
View File
@@ -118,7 +118,7 @@ const SideBar: FC = () => {
icon={<DocumentText size={iconSizeSideBar} color='#4F5260' />}
title={t('sidebar.criticisms')}
isActive={isActive('/criticisms')}
link={Paths.criticisms}
link={Paths.criticisms.list}
/>
<SideBarItem