diff --git a/.env b/.env index 4b17138..3fbd3a5 100644 --- a/.env +++ b/.env @@ -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' \ No newline at end of file +# VITE_BASE_URL = 'https://danak-dsc-api.run.danakcorp.com' +VITE_BASE_URL = 'http://192.168.0.207:3500' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1ef9c14..c137579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 655e1ed..5b81089 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx index 1b6ac87..fd37e49 100644 --- a/src/components/DatePicker.tsx +++ b/src/components/DatePicker.tsx @@ -31,7 +31,8 @@ const DatePickerComponent: FC = (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) { diff --git a/src/components/ModalConfrim.tsx b/src/components/ModalConfrim.tsx new file mode 100644 index 0000000..769ac1f --- /dev/null +++ b/src/components/ModalConfrim.tsx @@ -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) => { + + const { t } = useTranslation('global') + + return ( + +
+
{t('confrim.content')}
+ +
+
+
+
+ ) +} + +export default ModalConfrim \ No newline at end of file diff --git a/src/components/PageLoading.tsx b/src/components/PageLoading.tsx index c5353f0..4d24026 100644 --- a/src/components/PageLoading.tsx +++ b/src/components/PageLoading.tsx @@ -7,7 +7,7 @@ const PageLoading: FC = () => { const { t } = useTranslation('global') return ( -
+
logo
{t('loading')} diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 097f6a9..9446906 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -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", diff --git a/src/langs/fa.json b/src/langs/fa.json index 36f8b37..04f4839 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -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": "لغو" + } } diff --git a/src/pages/criticisms/Detail.tsx b/src/pages/criticisms/Detail.tsx new file mode 100644 index 0000000..69f632d --- /dev/null +++ b/src/pages/criticisms/Detail.tsx @@ -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 ( +
+ { + getDetail.isPending ? + + : + +
+ {t('criticisms.criticism')} + {' #' + getDetail.data?.data?.criticism?.numericId} +
+ +
+
+
+ {getDetail.data?.data?.criticism?.title} +
+ +

+ {getDetail.data?.data?.criticism?.content} +

+
+ +
+
+ {t('criticisms.documents')} +
+ +
+ { + getDetail.data?.data?.criticism?.files?.map((item: { id: string, fileUrl: string }) => { + return ( + + +
+ {item.fileUrl.split('/')[item.fileUrl.split('/').length - 1]} +
+
+ ) + }) + } +
+
+
+
+ } +
+ ) +} + +export default CriticismsDetail \ No newline at end of file diff --git a/src/pages/criticisms/List.tsx b/src/pages/criticisms/List.tsx new file mode 100644 index 0000000..3a04327 --- /dev/null +++ b/src/pages/criticisms/List.tsx @@ -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('') + const [page, setPage] = useState(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 ( +
+
+ {t('criticisms.criticisms_list')} +
+ +
+ + + + + + + { + getCriticismsList.data?.data?.criticisms?.map((item: CriticismsItemTypes) => { + return ( + + + + + ) + }) + } + + +
+ + + + + +
+ + + + + + + + + handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' /> +
+ +
+ +
+ + setOpenConfrim(false)} + onConfrim={() => handleDelete('', true)} + isLoading={deleteCriticisms.isPending} + /> +
+
+ ) +} + +export default CriticismsList \ No newline at end of file diff --git a/src/pages/criticisms/hooks/useCriticismsData.ts b/src/pages/criticisms/hooks/useCriticismsData.ts new file mode 100644 index 0000000..55b4579 --- /dev/null +++ b/src/pages/criticisms/hooks/useCriticismsData.ts @@ -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, + }); +}; diff --git a/src/pages/criticisms/service/CriticismsService.ts b/src/pages/criticisms/service/CriticismsService.ts new file mode 100644 index 0000000..5c5a9a6 --- /dev/null +++ b/src/pages/criticisms/service/CriticismsService.ts @@ -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; +}; diff --git a/src/pages/criticisms/types/CriticismsTypes.ts b/src/pages/criticisms/types/CriticismsTypes.ts new file mode 100644 index 0000000..d55fcc6 --- /dev/null +++ b/src/pages/criticisms/types/CriticismsTypes.ts @@ -0,0 +1,8 @@ +export type CriticismsItemTypes = { + id: string; + content: string; + createdAt: Date; + isRead: boolean; + title: string; + numericId: string; +}; diff --git a/src/pages/service/AddService.tsx b/src/pages/service/AddService.tsx index 26fe62b..7c61952 100644 --- a/src/pages/service/AddService.tsx +++ b/src/pages/service/AddService.tsx @@ -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(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({ + 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 (
@@ -38,6 +78,7 @@ const AddService: FC = () => {
) diff --git a/src/pages/service/components/AddServiceSidebar.tsx b/src/pages/service/components/AddServiceSidebar.tsx new file mode 100644 index 0000000..4213cad --- /dev/null +++ b/src/pages/service/components/AddServiceSidebar.tsx @@ -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 +} + +const AddServiceSidebar: FC = (props: Props) => { + + const getCategory = useGetCategoryParents() + + const { formik } = props + + const { t } = useTranslation('global') + + return ( +
+
+
+ {t('service.service_status')} +
+
+ formik.setFieldValue('isDanakSuggest', e)} + /> +
+
+ +
+ formik.setFieldValue('isDanakSuggest', e.target.checked)} + /> +
{t('service.special_danak')}
+
+ +
+ +
+ +
+
{t('service.category')}
+
+ { + getCategory.data?.data?.categories?.map((item: ServiceCategoryType) => { + return ( +
+ formik.setFieldValue('categoryId', e.target.checked ? item.id : '')} + /> +
{item.title}
+
+ ) + }) + } + +
+
+ +
+
{t('service.service_icon')}
+
+ null} + /> +
+
+
+
{t('service.service_images')}
+
+ null} + /> +
+
+
+ ) +} + +export default AddServiceSidebar \ No newline at end of file diff --git a/src/pages/service/types/ServiceTypes.ts b/src/pages/service/types/ServiceTypes.ts index 34c987e..d036093 100644 --- a/src/pages/service/types/ServiceTypes.ts +++ b/src/pages/service/types/ServiceTypes.ts @@ -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; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 292e615..b81a20e 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -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 = () => { } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index 41d0d37..f88510a 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -164,7 +164,7 @@ const SideBar: FC = () => { icon={} title={t('sidebar.criticisms')} isActive={isActive('criticisms')} - link={Pages.criticisms} + link={Pages.criticisms.list} />