This commit is contained in:
hamid zarghami
2025-01-27 20:36:14 +03:30
parent 22482aa20a
commit ce0019fde0
18 changed files with 523 additions and 79 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
VITE_TOKEN_NAME = 'admin_token'
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
VITE_BASE_URL = 'http://192.168.0.223:3500'
# VITE_BASE_URL = 'https://danak-dsc-api.run.danakcorp.com'
VITE_BASE_URL = 'http://192.168.0.207:3500'
+50
View File
@@ -16,6 +16,7 @@
"formik": "^2.4.6",
"i18next": "^24.2.0",
"iconsax-react": "^0.0.8",
"moment-jalaali": "^0.10.4",
"quill": "^2.0.3",
"rc-rate": "^2.13.0",
"react": "^19.0.0",
@@ -36,6 +37,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/draft-js": "^0.11.18",
"@types/moment-jalaali": "^0.7.9",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"@types/swiper": "^5.4.3",
@@ -1855,6 +1857,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/moment-jalaali": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
"integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"moment": ">=2.14.0"
}
},
"node_modules/@types/node": {
"version": "22.10.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
@@ -3602,6 +3614,12 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jalaali-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.7.tgz",
"integrity": "sha512-gE+YHWSbygYAoJa+Xg8LWxGILqFOxZSBQQw39ghel01fVFUxV7bjL0x1JFsHcLQ3uPjvn81HQMa+kxwyPWnxGQ==",
"license": "MIT"
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -3865,6 +3883,38 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moment-jalaali": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz",
"integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==",
"license": "MIT",
"dependencies": {
"jalaali-js": "^1.2.7",
"moment": "^2.29.4",
"moment-timezone": "^0.5.46"
}
},
"node_modules/moment-timezone": {
"version": "0.5.46",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz",
"integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+2
View File
@@ -18,6 +18,7 @@
"formik": "^2.4.6",
"i18next": "^24.2.0",
"iconsax-react": "^0.0.8",
"moment-jalaali": "^0.10.4",
"quill": "^2.0.3",
"rc-rate": "^2.13.0",
"react": "^19.0.0",
@@ -38,6 +39,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/draft-js": "^0.11.18",
"@types/moment-jalaali": "^0.7.9",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"@types/swiper": "^5.4.3",
+2 -1
View File
@@ -31,7 +31,8 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
const formattedDate = `${value.year}/${value.month.number}/${value.day}`;
props.onChange(formattedDate);
}
}, [props, value]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
useEffect(() => {
if (props.defaulValue && !value) {
+44
View File
@@ -0,0 +1,44 @@
import { FC } from 'react'
import DefaulModal from './DefaulModal'
import { useTranslation } from 'react-i18next'
import Button from './Button'
type Props = {
isLoading?: boolean,
close: () => void,
isOpen: boolean,
onConfrim: () => void,
}
const ModalConfrim: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
return (
<DefaulModal
open={props.isOpen}
close={props.close}
title_header={t('confrim.subject')}
isHeader
>
<div className='mt-6'>
<div className='text-sm text-center'> {t('confrim.content')}</div>
<div className='flex gap-4 justify-center mt-10'>
<Button
label={t('confrim.yes')}
onClick={props.onConfrim}
isLoading={props.isLoading}
/>
<Button
label={t('confrim.cancel')}
className='bg-transparent text-black border border-primary'
onClick={props.close}
/>
</div>
</div>
</DefaulModal>
)
}
export default ModalConfrim
+1 -1
View File
@@ -7,7 +7,7 @@ const PageLoading: FC = () => {
const { t } = useTranslation('global')
return (
<div className='flex bg-white w-fit mx-auto flex-col gap-4 items-center'>
<div className='flex bg-transparent w-fit mx-auto flex-col gap-4 items-center'>
<img src={Logo} alt='logo' className='w-28' />
<div className='text-xs text-gray-700'>
{t('loading')}
+4 -1
View File
@@ -29,7 +29,10 @@ export const Pages = {
list: "/announcement",
detail: "/announcement/",
},
criticisms: "/criticisms",
criticisms: {
list: "/criticisms/list",
detail: "/ccriticisms/detail/",
},
learning: "/learning",
setting: "/setting",
wallet: "/wallet",
+16 -2
View File
@@ -375,8 +375,16 @@
"subject": "موضوع",
"enter_your_subject": "موضوع انتقاد خودرا وارد کنید",
"description": "توضیحات",
"enter_description": "توضیحات شما"
"enter_description": "توضیحات شما",
"criticisms_list": "لیست انتقادات",
"number": "شماره",
"title_criticisms": "عنوان انتقاد",
"summary_criticisms": "خلاصه انتقاد",
"date_created": "تاریخ ثبت",
"criticism": "انتقاد",
"documents": "مستندات"
},
"detail": "جزییات",
"select_file": "انتخاب فایل",
"no_select_file": "هیچ فایلی انتخاب نشده است",
"attachment": "فایل های ضمیمه",
@@ -485,5 +493,11 @@
"date": "تاریخ"
},
"loading": "در حال بارگذاری...",
"success": "عملیات با موفقیت انجام شد."
"success": "عملیات با موفقیت انجام شد.",
"confrim": {
"subject": "حذف",
"content": "برای حذف این آیتم مطمئن هستید؟",
"yes": "بله",
"cancel": "لغو"
}
}
+67
View File
@@ -0,0 +1,67 @@
import { 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 getDetail = useGetDetailCriticisms(id ? id : '')
return (
<div className='mt-4'>
{
getDetail.isPending ?
<PageLoading />
:
<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
+101
View File
@@ -0,0 +1,101 @@
import { FC, useState } from 'react'
import { useDeleteCriticismsDetail, useGetCriticismsList } from './hooks/useCriticismsData'
import { useTranslation } from 'react-i18next'
import Td from '../../components/Td'
import { Link } from 'react-router-dom'
import { Eye, Trash } from 'iconsax-react'
import { Pages } from '../../config/Pages'
import { CriticismsItemTypes } from './types/CriticismsTypes'
import moment from 'moment-jalaali'
import ModalConfrim from '../../components/ModalConfrim'
import Pagination from '../../components/Pagination'
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 handleDelete = (id?: string, confrim: boolean = false) => {
if (confrim) {
deleteCriticisms.mutate(itemId, {
onSuccess: () => {
getCriticismsList.refetch()
setOpenConfrim(false)
setItemId('')
}
})
} else if (id) {
setItemId(id)
setOpenConfrim(true)
}
}
return (
<div className='mt-4'>
<div>
{t('criticisms.criticisms_list')}
</div>
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<table className='w-full text-sm '>
<thead className='thead'>
<tr>
<Td text={t('criticisms.number')} />
<Td text={t('criticisms.title_criticisms')} />
<Td text={t('criticisms.summary_criticisms')} />
<Td text={t('criticisms.date_created')} />
<Td text={t('detail')} />
<Td text={''} />
</tr>
</thead>
<tbody>
{
getCriticismsList.data?.data?.criticisms?.map((item: CriticismsItemTypes) => {
return (
<tr key={item.id} className='tr'>
<Td text={item.numericId} />
<Td text={item.title} />
<Td text={item.content.substring(0, 100)} />
<Td text={moment(item.createdAt).format('HH:mm jYYYY-jMM-jDD')} />
<Td text={''}>
<Link to={Pages.criticisms.detail + item.id}>
<Eye size={20} color='#8C90A3' />
</Link>
</Td>
<Td text=''>
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
</Td>
</tr>
)
})
}
</tbody>
</table>
<div className='flex justify-end pb-3'>
<Pagination
currentPage={page}
totalPages={getCriticismsList.data?.data?.pager?.totalPages}
onPageChange={setPage}
/>
</div>
<ModalConfrim
isOpen={openConfrim}
close={() => setOpenConfrim(false)}
onConfrim={() => handleDelete('', true)}
isLoading={deleteCriticisms.isPending}
/>
</div>
</div>
)
}
export default CriticismsList
@@ -0,0 +1,23 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/CriticismsService";
export const useGetCriticismsList = (page: number) => {
return useQuery({
queryKey: ["criticisms", page],
queryFn: () => api.getCriticismsList(page),
});
};
export const useDeleteCriticismsDetail = () => {
return useMutation({
mutationFn: (id: string) => api.deleteCriticisms(id),
});
};
export const useGetDetailCriticisms = (id: string) => {
return useQuery({
queryKey: ["criticisms", id],
queryFn: () => api.getCriticismsDetail(id),
enabled: !!id,
});
};
@@ -0,0 +1,16 @@
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,8 @@
export type CriticismsItemTypes = {
id: string;
content: string;
createdAt: Date;
isRead: boolean;
title: string;
numericId: string;
};
+70 -70
View File
@@ -1,20 +1,23 @@
import { FC, useEffect } from 'react'
import { FC, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../components/Input'
import Select from '../../components/Select'
import Quill from 'quill';
import SwitchComponent from '../../components/Switch';
import CheckBoxComponent from '../../components/CheckBoxComponent';
import UploadBoxDraggble from '../../components/UploadBoxDraggble';
import Button from '../../components/Button';
import { TickCircle } from 'iconsax-react';
import { useFormik } from 'formik';
import { CreateServiceType } from './types/ServiceTypes';
import DatePickerComponent from '../../components/DatePicker';
import AddServiceSidebar from './components/AddServiceSidebar';
import * as Yup from 'yup'
const AddService: FC = () => {
const { t } = useTranslation('global')
const editorRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
new Quill('#editor', {
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
@@ -27,8 +30,45 @@ const AddService: FC = () => {
]
}
});
quill.on('text-change', () => {
const html = editorRef.current?.querySelector('.ql-editor')?.innerHTML || '';
formik.setFieldValue('metaDescription', html);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formik = useFormik<CreateServiceType>({
initialValues: {
author: '',
categoryId: '',
description: '',
icon: '',
images: [],
isDanakSuggest: false,
link: '',
metaDescription: '',
name: '',
serviceLanguage: '',
softwareLanguage: '',
userCount: 0,
createDate: ''
},
validationSchema: Yup.object({
name: Yup.string().required(t('errors.required')),
description: Yup.string().required(t('errors.required')),
author: Yup.string().required(t('errors.required')),
createDate: Yup.date().required(t('errors.required')),
userCount: Yup.number().required(t('errors.required')),
serviceLanguage: Yup.string().required(t('errors.required')),
metaDescription: Yup.string().required(t('errors.required')),
link: Yup.string().required(t('errors.required')),
}),
onSubmit(values) {
console.log(values);
},
})
return (
<div className='w-full mt-4'>
<div className='flex w-full justify-between items-center'>
@@ -38,6 +78,7 @@ const AddService: FC = () => {
<div>
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
>
<div className='flex gap-2'>
<TickCircle
@@ -56,25 +97,41 @@ const AddService: FC = () => {
<div className='flex-1 mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<Input
label={t('service.service_name')}
name='name'
onChange={formik.handleChange}
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
/>
<div className='mt-8'>
<Input
label={t('service.description_short')}
name='description'
onChange={formik.handleChange}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
/>
</div>
<div className='mt-8 rowTwoInput'>
<Input
label={t('service.company_developed')}
name='author'
onChange={formik.handleChange}
error_text={formik.touched.author && formik.errors.author ? formik.errors.author : ''}
/>
<Input
<DatePickerComponent
label={t('service.year_make')}
onChange={(d) => formik.setFieldValue('createDate', d)}
placeholder={t('select')}
error_text={formik.touched.createDate && formik.errors.createDate ? formik.errors.createDate : ''}
/>
</div>
<div className='mt-8 rowTwoInput'>
<Input
label={t('service.user_count')}
name='userCount'
onChange={formik.handleChange}
error_text={formik.touched.userCount && formik.errors.userCount ? formik.errors.userCount : ''}
/>
<Select
@@ -86,78 +143,21 @@ const AddService: FC = () => {
}
]}
placeholder={t('select')}
name='serviceLanguage'
onChange={formik.handleChange}
error_text={formik.touched.serviceLanguage && formik.errors.serviceLanguage ? formik.errors.serviceLanguage : ''}
/>
</div>
<div className='mt-8'>
<div id='editor' style={{ minHeight: '120px' }}></div>
<div ref={editorRef} id='editor' style={{ minHeight: '120px' }}></div>
</div>
</div>
</div>
<div className='bg-white mt-10 w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
<div className='flex justify-between'>
<div className='text-sm'>
{t('service.service_status')}
</div>
<div className='flex gap-1'>
<SwitchComponent active onChange={() => null} />
</div>
</div>
<div className='mt-8 items-center flex '>
<CheckBoxComponent
checked
onChange={() => null}
/>
<div>{t('service.special_danak')}</div>
</div>
<div className='mt-8'>
<Input
label={t('service.service_link')}
/>
</div>
<div className='mt-8'>
<div>{t('service.category')}</div>
<div className='mt-2 border border-border rounded-xl p-4'>
<div className='flex items-center'>
<CheckBoxComponent
checked
onChange={() => null}
/>
<div>لورم ایپسوم</div>
</div>
<div className='flex items-center'>
<CheckBoxComponent
checked
onChange={() => null}
/>
<div>لورم ایپسوم</div>
</div>
</div>
</div>
<div className='mt-8'>
<div>{t('service.service_icon')}</div>
<div className='mt-2'>
<UploadBoxDraggble
label={t('service.upload_icon_service')}
onChange={() => null}
/>
</div>
</div>
<div className='mt-8'>
<div>{t('service.service_images')}</div>
<div className='mt-2'>
<UploadBoxDraggble
label={t('service.upload_images_service')}
onChange={() => null}
/>
</div>
</div>
</div>
<AddServiceSidebar
formik={formik}
/>
</div>
</div>
)
@@ -0,0 +1,96 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import SwitchComponent from '../../../components/Switch'
import CheckBoxComponent from '../../../components/CheckBoxComponent'
import Input from '../../../components/Input'
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'
import { FormikProps } from 'formik'
import { CreateServiceType, ServiceCategoryType } from '../types/ServiceTypes'
import { useGetCategoryParents } from '../hooks/useServiceData'
type Props = {
formik: FormikProps<CreateServiceType>
}
const AddServiceSidebar: FC<Props> = (props: Props) => {
const getCategory = useGetCategoryParents()
const { formik } = props
const { t } = useTranslation('global')
return (
<div className='bg-white mt-10 w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
<div className='flex justify-between'>
<div className='text-sm'>
{t('service.service_status')}
</div>
<div className='flex gap-1'>
<SwitchComponent
active={formik.values.isDanakSuggest}
onChange={(e) => formik.setFieldValue('isDanakSuggest', e)}
/>
</div>
</div>
<div className='mt-8 items-center flex '>
<CheckBoxComponent
checked={formik.values.isDanakSuggest}
onChange={(e) => formik.setFieldValue('isDanakSuggest', e.target.checked)}
/>
<div>{t('service.special_danak')}</div>
</div>
<div className='mt-8'>
<Input
label={t('service.service_link')}
name='link'
onChange={formik.handleChange}
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ''}
/>
</div>
<div className='mt-8'>
<div>{t('service.category')}</div>
<div className='mt-2 border border-border rounded-xl p-4'>
{
getCategory.data?.data?.categories?.map((item: ServiceCategoryType) => {
return (
<div key={item.id} className='flex items-center'>
<CheckBoxComponent
checked={formik.values.categoryId === item.id}
onChange={(e) => formik.setFieldValue('categoryId', e.target.checked ? item.id : '')}
/>
<div>{item.title}</div>
</div>
)
})
}
</div>
</div>
<div className='mt-8'>
<div>{t('service.service_icon')}</div>
<div className='mt-2'>
<UploadBoxDraggble
label={t('service.upload_icon_service')}
onChange={() => null}
/>
</div>
</div>
<div className='mt-8'>
<div>{t('service.service_images')}</div>
<div className='mt-2'>
<UploadBoxDraggble
label={t('service.upload_images_service')}
onChange={() => null}
/>
</div>
</div>
</div>
)
}
export default AddServiceSidebar
+16
View File
@@ -25,3 +25,19 @@ export type CreateServiceCategoryType = {
export type ToggleStatusCategoryType = {
id: string;
};
export type CreateServiceType = {
categoryId: string;
name: string;
description: string;
author: string;
userCount: number;
serviceLanguage: string;
softwareLanguage: string;
metaDescription: string;
link: string;
isDanakSuggest: boolean;
icon?: string;
images?: string[];
createDate: string;
};
+4 -2
View File
@@ -18,7 +18,6 @@ import TicketDetail from '../pages/ticket/Detail'
import TransactionList from '../pages/transaction/List'
import ReceiptsList from '../pages/receipts/List'
import AnnouncementtList from '../pages/annoncement/List'
import AddCriticisms from '../pages/criticisms/Add'
import LearningList from '../pages/learning/List'
import Setting from '../pages/setting/Setting'
import Wallet from '../pages/wallet/Wallet'
@@ -38,6 +37,8 @@ import CustomerList from '../pages/customer/List'
import CreateCustomer from '../pages/customer/Create'
import TicketCategory from '../pages/ticket/Category'
import MessagesList from '../pages/messages/List'
import CriticismsList from '../pages/criticisms/List'
import CriticismsDetail from '../pages/criticisms/Detail'
const MainRouter: FC = () => {
@@ -80,7 +81,8 @@ const MainRouter: FC = () => {
<Route path={Pages.receipts.create} element={<CreateReceipt />} />
<Route path={Pages.announcement.list} element={<AnnouncementtList />} />
<Route path={Pages.announcement.detail + ':id'} element={<AnnouncementDetail />} />
<Route path={Pages.criticisms} element={<AddCriticisms />} />
<Route path={Pages.criticisms.list} element={<CriticismsList />} />
<Route path={Pages.criticisms.detail + ':id'} element={<CriticismsDetail />} />
<Route path={Pages.learning} element={<LearningList />} />
<Route path={Pages.setting} element={<Setting />} />
<Route path={Pages.wallet} element={<Wallet />} />
+1 -1
View File
@@ -164,7 +164,7 @@ const SideBar: FC = () => {
icon={<DocumentText variant={isActive('criticisms') ? 'Bold' : 'Outline'} color={isActive('criticisms') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.criticisms')}
isActive={isActive('criticisms')}
link={Pages.criticisms}
link={Pages.criticisms.list}
/>
</div>