address
This commit is contained in:
@@ -4,3 +4,5 @@ VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
|
|||||||
# VITE_BASE_URL = 'http://192.168.1.134:4000'
|
# VITE_BASE_URL = 'http://192.168.1.134:4000'
|
||||||
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
||||||
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
||||||
|
|
||||||
|
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
|
||||||
|
|||||||
+13
-10
@@ -1,13 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<title>Vite + React + TS</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
</head>
|
<title>Dmail Admin | داناک</title>
|
||||||
<body>
|
</head>
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<body>
|
||||||
</body>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+3
-1
@@ -1 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
<svg width="85" height="72" viewBox="0 0 85 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M47.97 0C39.21 0 20.6 0.0500073 20.18 0.0700073C28.87 8.77001 45.47 25.11 55.3 34.9C54.27 35.93 53.22 36.98 52.11 38.09C49.1 41.1 47.07 41.82 44.03 38.78C31.57 26.32 5.38 0.07 5.28 0H0V71.79C0 71.79 11.97 71.77 12.57 71.77V24.82C12.63 24.6 16.87 28.77 22.08 33.96L21.63 71.81C22.28 71.81 39.72 71.83 47.28 71.8C49.16 71.8 51.06 71.69 52.93 71.49C69.23 69.67 82.67 55.87 84.14 39.48C86.07 17.95 69.62 0 47.98 0H47.97ZM46.3 11.58C55.58 10.72 63.99 15.5 67.84 22.39C66.04 24.19 64.36 25.86 62.72 27.49C57.77 22.69 49.24 14.43 46.3 11.58ZM48.3 60.07C45.38 60.12 42.46 60.12 39.54 60.11H31.86V43.77C33.94 45.85 35.86 47.78 37.43 49.33C43.64 55.49 53.12 52.88 59.33 46.75C63.62 42.51 71.94 34.1 72.57 33.51C73.5 47.57 62.46 59.84 48.29 60.09L48.3 60.07Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 876 B |
+1
-1
@@ -81,7 +81,7 @@ const queryClient = new QueryClient({
|
|||||||
// Clear tokens and redirect to login
|
// Clear tokens and redirect to login
|
||||||
await removeToken();
|
await removeToken();
|
||||||
await removeRefreshToken();
|
await removeRefreshToken();
|
||||||
window.location.href = '/auth/login';
|
window.location.href = import.meta.env.VITE_LOGIN_URL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import DefaulModal from './DefaulModal'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from './Button'
|
||||||
|
import Textarea from './Textarea'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isLoading?: boolean,
|
||||||
|
close: () => void,
|
||||||
|
isOpen: boolean,
|
||||||
|
onConfrim: (text?: string) => void,
|
||||||
|
label?: string,
|
||||||
|
isHasDescription?: boolean,
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalConfrim: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [description, setDescription] = useState<string>('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={props.isOpen}
|
||||||
|
close={props.close}
|
||||||
|
title_header={props.title ? props.title : t('confrim.subject')}
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className='text-sm text-center'>
|
||||||
|
{
|
||||||
|
props.label ?
|
||||||
|
props.label
|
||||||
|
:
|
||||||
|
t('confrim.content')
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
props.isHasDescription &&
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t('description')}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
value={description}
|
||||||
|
className='bg-transparent border border-gray-500 rounded-xl w-[300px] p-2'
|
||||||
|
label=''
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-4 justify-center mt-10'>
|
||||||
|
<Button
|
||||||
|
label={t('confrim.yes')}
|
||||||
|
onClick={() => props.onConfrim(props.isHasDescription ? description : undefined)}
|
||||||
|
loading={props.isLoading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={t('confrim.cancel')}
|
||||||
|
className='bg-transparent text-black border border-primary'
|
||||||
|
onClick={props.close}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalConfrim
|
||||||
@@ -101,6 +101,11 @@
|
|||||||
"smtp_port": "SMTP پورت",
|
"smtp_port": "SMTP پورت",
|
||||||
"username": "نام کاربری",
|
"username": "نام کاربری",
|
||||||
"password": "پسورد",
|
"password": "پسورد",
|
||||||
|
"setting_imap": "تنظیمات IMAP",
|
||||||
|
"setting_imap_description": "تنظیمات IMAP خود را با دقت وارد کنید.",
|
||||||
|
"imap_server": "IMAP سرور",
|
||||||
|
"imap_port": "IMAP پورت",
|
||||||
|
"encryption": "نوع رمزنگاری",
|
||||||
"record_dns": "رکوردهای DNS",
|
"record_dns": "رکوردهای DNS",
|
||||||
"record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.",
|
"record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.",
|
||||||
"your_domain": "دامنه شما",
|
"your_domain": "دامنه شما",
|
||||||
@@ -166,5 +171,13 @@
|
|||||||
"answer": "پاسخ",
|
"answer": "پاسخ",
|
||||||
"forward": "ارسال به دیگران",
|
"forward": "ارسال به دیگران",
|
||||||
"message": "متن پبام"
|
"message": "متن پبام"
|
||||||
|
},
|
||||||
|
"confrim": {
|
||||||
|
"subject": "تایید عملیات",
|
||||||
|
"content": "آیا از انجام این عملیات مطمئن هستید؟",
|
||||||
|
"yes": "بله",
|
||||||
|
"cancel": "انصراف",
|
||||||
|
"delete_address_title": "حذف آدرس",
|
||||||
|
"delete_address_message": "آیا از حذف آدرس {email} مطمئن هستید؟"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,44 +5,149 @@ import { ColumnType } from '@/components/types/TableTypes'
|
|||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { TickCircle } from 'iconsax-react'
|
import { TickCircle, Edit2, Trash } from 'iconsax-react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { AddressData, CreateAddressType } from './types/Types'
|
import { AddressData, CreateAddressType, UpdateAddressType } from './types/Types'
|
||||||
import { useGetDomains } from '../domain/hooks/useDomainData'
|
import { useGetDomains } from '../domain/hooks/useDomainData'
|
||||||
import { yupResolver } from "@hookform/resolvers/yup"
|
import { useCreateAddress, useGetAddress, useUpdateAddress, useDeleteAddress } from './hooks/useAddressData'
|
||||||
import * as yup from "yup"
|
import { useGetSpace } from '../domain/hooks/useDomainData'
|
||||||
import { useCreateAddress, useGetAddress } from './hooks/useAddressData'
|
|
||||||
import { toast } from '@/components/Toast'
|
import { toast } from '@/components/Toast'
|
||||||
import { ErrorType } from '@/helpers/types'
|
import { ErrorType } from '@/helpers/types'
|
||||||
import ProgressBarEmail from './components/ProgressBarEmail'
|
import ProgressBarEmail from './components/ProgressBarEmail'
|
||||||
import ToggleStatus from './components/ToggleStatus'
|
import ToggleStatus from './components/ToggleStatus'
|
||||||
|
import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown'
|
||||||
|
import ModalConfrim from '@/components/ModalConfrim'
|
||||||
|
|
||||||
const Address: FC = () => {
|
const Address: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const schema = yup.object().shape({
|
|
||||||
title: yup.string().required(t('errors.required')),
|
|
||||||
username: yup.string().required(t('errors.required')),
|
|
||||||
password: yup.string().required(t('errors.required')),
|
|
||||||
domainId: yup.string().required(t('errors.required')),
|
|
||||||
})
|
|
||||||
|
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const [status, setStatus] = useState<string>('all')
|
const [status, setStatus] = useState<string>('all')
|
||||||
|
const [currentPage, setCurrentPage] = useState<number>(1)
|
||||||
|
const [selectedAddress, setSelectedAddress] = useState<AddressData | null>(null)
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false)
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||||
|
const [addressToDelete, setAddressToDelete] = useState<AddressData | null>(null)
|
||||||
|
|
||||||
const { data: domain } = useGetDomains()
|
const { data: domain } = useGetDomains()
|
||||||
|
const { data: spaceData } = useGetSpace()
|
||||||
const { mutate: createAddress, isPending } = useCreateAddress()
|
const { mutate: createAddress, isPending } = useCreateAddress()
|
||||||
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status)
|
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
|
||||||
const { register, handleSubmit, formState: { errors }, setValue
|
const { mutate: deleteAddress } = useDeleteAddress()
|
||||||
} = useForm<CreateAddressType>({
|
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10)
|
||||||
resolver: yupResolver(schema),
|
|
||||||
|
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors
|
||||||
|
} = useForm<FormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
domainId: domain?.data?.domain?.id,
|
domainId: domain?.data?.domain?.id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const validateForm = (params: FormData): boolean => {
|
||||||
|
clearErrors()
|
||||||
|
let isValid = true
|
||||||
|
|
||||||
|
if (!params.title) {
|
||||||
|
setError('title', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.username) {
|
||||||
|
setError('username', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEditMode && !params.password) {
|
||||||
|
setError('password', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.domainId) {
|
||||||
|
setError('domainId', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// اعتبارسنجی quota
|
||||||
|
if (params.quotaMB) {
|
||||||
|
const quotaInBytes = params.quotaMB * 1024 * 1024
|
||||||
|
const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
if (quotaInBytes > remainingSpace) {
|
||||||
|
setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقیمانده است' })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (address: AddressData) => {
|
||||||
|
setSelectedAddress(address)
|
||||||
|
setIsEditMode(true)
|
||||||
|
reset({
|
||||||
|
title: address.title,
|
||||||
|
username: address.displayName,
|
||||||
|
password: '',
|
||||||
|
domainId: address.domain.id,
|
||||||
|
quotaMB: address.emailQuota ? Math.round(address.emailQuota / (1024 * 1024)) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (address: AddressData) => {
|
||||||
|
setAddressToDelete(address)
|
||||||
|
setIsDeleteModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (addressToDelete) {
|
||||||
|
deleteAddress(addressToDelete.id, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success')
|
||||||
|
setIsDeleteModalOpen(false)
|
||||||
|
setAddressToDelete(null)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error')
|
||||||
|
setIsDeleteModalOpen(false)
|
||||||
|
setAddressToDelete(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseDeleteModal = () => {
|
||||||
|
setIsDeleteModalOpen(false)
|
||||||
|
setAddressToDelete(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setSelectedAddress(null)
|
||||||
|
setIsEditMode(false)
|
||||||
|
reset({
|
||||||
|
title: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
domainId: domain?.data?.domain?.id,
|
||||||
|
quotaMB: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRowActions = (address: AddressData): RowActionItem[] => [
|
||||||
|
{
|
||||||
|
label: 'ویرایش',
|
||||||
|
icon: <Edit2 color='black' size={16} />,
|
||||||
|
onClick: () => handleEdit(address),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'حذف',
|
||||||
|
icon: <Trash color='red' size={16} />,
|
||||||
|
onClick: () => handleDelete(address),
|
||||||
|
className: 'text-red-600 hover:bg-red-50'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const columns: ColumnType<AddressData>[] = [
|
const columns: ColumnType<AddressData>[] = [
|
||||||
{
|
{
|
||||||
title: t('setting.address_title'),
|
title: t('setting.address_title'),
|
||||||
@@ -64,7 +169,6 @@ const Address: FC = () => {
|
|||||||
key: 'quota',
|
key: 'quota',
|
||||||
width: '300px',
|
width: '300px',
|
||||||
render: (item: AddressData) => {
|
render: (item: AddressData) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgressBarEmail key={item.id} item={item} />
|
<ProgressBarEmail key={item.id} item={item} />
|
||||||
)
|
)
|
||||||
@@ -80,6 +184,18 @@ const Address: FC = () => {
|
|||||||
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
|
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'actions',
|
||||||
|
width: '120px',
|
||||||
|
align: 'center',
|
||||||
|
render: (item: AddressData) => (
|
||||||
|
<RowActionsDropdown
|
||||||
|
key={item.id}
|
||||||
|
actions={getRowActions(item)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -89,24 +205,52 @@ const Address: FC = () => {
|
|||||||
{ value: 'inactive', label: t('setting.inactive') }
|
{ value: 'inactive', label: t('setting.inactive') }
|
||||||
]
|
]
|
||||||
|
|
||||||
const onSubmit = (params: CreateAddressType) => {
|
const onSubmit = (params: FormData) => {
|
||||||
createAddress(params, {
|
if (!validateForm(params)) {
|
||||||
onSuccess: (data) => {
|
return
|
||||||
toast(data?.data?.message, 'success')
|
}
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
if (isEditMode && selectedAddress) {
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
const updateParams: UpdateAddressType = {
|
||||||
|
title: params.title,
|
||||||
|
password: params.password || undefined,
|
||||||
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
}
|
}
|
||||||
})
|
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
|
||||||
|
handleCancelEdit()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const createParams: CreateAddressType = {
|
||||||
|
...params,
|
||||||
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
|
}
|
||||||
|
createAddress(createParams, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message, 'success')
|
||||||
|
reset()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (domain?.data?.domain?.id) {
|
if (domain?.data?.domain?.id && !isEditMode) {
|
||||||
setValue('domainId', domain?.data?.domain?.id)
|
setValue('domainId', domain?.data?.domain?.id)
|
||||||
}
|
}
|
||||||
}, [domain])
|
}, [domain, isEditMode])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1)
|
||||||
|
}, [search, status])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
|
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
|
||||||
@@ -136,26 +280,28 @@ const Address: FC = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={addresses?.data?.users}
|
data={addresses?.data?.users}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage: 1,
|
currentPage: currentPage,
|
||||||
totalPages: 6,
|
totalPages: addresses?.data?.pager?.totalPages || 1,
|
||||||
onPageChange: (page) => console.log('Page:', page)
|
onPageChange: (page) => setCurrentPage(page)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
|
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
|
||||||
<div>
|
<div className='flex justify-between items-center'>
|
||||||
{t('setting.add_address')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className='mt-8 flex items-center justify-between'>
|
|
||||||
<div>
|
<div>
|
||||||
{t('setting.status')}
|
{isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
|
||||||
</div>
|
</div>
|
||||||
<div className='dltr pt-2'>
|
{isEditMode && (
|
||||||
<Switch />
|
<Button
|
||||||
</div>
|
variant='secondary'
|
||||||
</div> */}
|
onClick={handleCancelEdit}
|
||||||
|
className='px-4 w-fit text-xs'
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<Input
|
<Input
|
||||||
@@ -169,7 +315,7 @@ const Address: FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
label={t('setting.title1')}
|
label={t('setting.title1')}
|
||||||
{...register('title')}
|
{...register('title')}
|
||||||
error_text={errors.title?.message}
|
error_text={errors.title?.message as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,7 +324,8 @@ const Address: FC = () => {
|
|||||||
label={t('setting.username')}
|
label={t('setting.username')}
|
||||||
className='text-left'
|
className='text-left'
|
||||||
{...register('username')}
|
{...register('username')}
|
||||||
error_text={errors.username?.message}
|
error_text={errors.username?.message as string}
|
||||||
|
readOnly={isEditMode}
|
||||||
/>
|
/>
|
||||||
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
|
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
|
||||||
@ {domain?.data?.domain?.name}
|
@ {domain?.data?.domain?.name}
|
||||||
@@ -187,38 +334,65 @@ const Address: FC = () => {
|
|||||||
|
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.password')}
|
label='حجم ایمیل (مگابایت)'
|
||||||
|
type='number'
|
||||||
|
{...register('quotaMB', { valueAsNumber: true })}
|
||||||
|
error_text={errors.quotaMB?.message as string}
|
||||||
|
placeholder='حجم ایمیل را به مگابایت وارد کنید'
|
||||||
|
/>
|
||||||
|
<div className='text-xs text-gray-500 mt-1'>
|
||||||
|
فضای باقیمانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
|
||||||
type='password'
|
type='password'
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
error_text={errors.password?.message}
|
error_text={errors.password?.message as string}
|
||||||
|
placeholder={isEditMode ? 'برای تغییر رمز عبور وارد کنید' : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-2 text-xs'>
|
|
||||||
<div>رمز عبور میبایست:</div>
|
|
||||||
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
|
{!isEditMode && (
|
||||||
<li>حداقل ۸ کاراکتر باشد</li>
|
<div className='mt-2 text-xs'>
|
||||||
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
|
<div>رمز عبور میبایست:</div>
|
||||||
<li>شامل اعداد باشد</li>
|
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
|
||||||
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
|
<li>حداقل ۸ کاراکتر باشد</li>
|
||||||
</ul>
|
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
|
||||||
</div>
|
<li>شامل اعداد باشد</li>
|
||||||
|
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='mt-9 flex justify-end'>
|
<div className='mt-9 flex justify-end'>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-20'
|
className='w-fit px-20'
|
||||||
onClick={handleSubmit(onSubmit)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
loading={isPending}
|
loading={isPending || isUpdating}
|
||||||
>
|
>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<TickCircle size={20} color='white' />
|
<TickCircle size={20} color='white' />
|
||||||
<div>
|
<div>
|
||||||
{t('setting.create')}
|
{isEditMode ? 'آپدیت' : t('setting.create')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
close={handleCloseDeleteModal}
|
||||||
|
onConfrim={handleConfirmDelete}
|
||||||
|
title={t('confrim.delete_address_title')}
|
||||||
|
label={addressToDelete ? t('confrim.delete_address_message').replace('{email}', addressToDelete.emailAddress) : ''}
|
||||||
|
isLoading={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import * as api from "../service/Service";
|
import * as api from "../service/Service";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { UpdateAddressType } from "../types/Types";
|
||||||
|
|
||||||
export const useGetAddress = (search: string, status: string) => {
|
export const useGetAddress = (
|
||||||
|
search: string,
|
||||||
|
status: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["address", search, status],
|
queryKey: ["address", search, status, page, limit],
|
||||||
queryFn: () => api.getAddress(search, status),
|
queryFn: () => api.getAddress(search, status, page, limit),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,7 +26,32 @@ export const useCreateAddress = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useToggleStatus = () => {
|
export const useToggleStatus = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: api.toggleStatus,
|
mutationFn: api.toggleStatus,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["address"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateAddress = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, params }: { id: string; params: UpdateAddressType }) =>
|
||||||
|
api.updateAddress(id, params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["address"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAddress = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteAddress,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["address"] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import axios from "@/config/axios";
|
import axios from "@/config/axios";
|
||||||
import { CreateAddressType } from "../types/Types";
|
import { CreateAddressType, UpdateAddressType } from "../types/Types";
|
||||||
|
|
||||||
export const getAddress = async (search: string, status: string) => {
|
export const getAddress = async (
|
||||||
|
search: string,
|
||||||
|
status: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
) => {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (search) query.set("q", search);
|
if (search) query.set("q", search);
|
||||||
if (status && status !== "all")
|
if (status && status !== "all")
|
||||||
query.set("isActive", status === "active" ? "1" : "0");
|
query.set("isActive", status === "active" ? "1" : "0");
|
||||||
|
query.set("page", page.toString());
|
||||||
|
query.set("limit", limit.toString());
|
||||||
const { data } = await axios.get(`/users?${query.toString()}`);
|
const { data } = await axios.get(`/users?${query.toString()}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -19,3 +26,13 @@ export const toggleStatus = async (id: string) => {
|
|||||||
const { data } = await axios.patch(`/users/${id}/toggle-status`);
|
const { data } = await axios.patch(`/users/${id}/toggle-status`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateAddress = async (id: string, params: UpdateAddressType) => {
|
||||||
|
const { data } = await axios.patch(`/users/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAddress = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/users/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ export type CreateAddressType = {
|
|||||||
password: string;
|
password: string;
|
||||||
domainId: string;
|
domainId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
quota?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateAddressType = {
|
||||||
|
password?: string;
|
||||||
|
displayName?: string;
|
||||||
|
quota?: number;
|
||||||
|
aliases?: string[];
|
||||||
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AddressData extends Record<string, unknown> {
|
export interface AddressData extends Record<string, unknown> {
|
||||||
@@ -56,3 +65,15 @@ export interface AddressData extends Record<string, unknown> {
|
|||||||
business: string;
|
business: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddressResponse {
|
||||||
|
data: {
|
||||||
|
users: AddressData[];
|
||||||
|
pager: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,46 +1,39 @@
|
|||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import RadioGroup from '@/components/RadioGroup'
|
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useMailServerData } from './hooks/useMailServerData'
|
||||||
|
|
||||||
const Domain: FC = () => {
|
const MailServer: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
const { data, isLoading, error } = useMailServerData();
|
||||||
<div className='bg-white rounded-4xl p-8 mt-9'>
|
|
||||||
<div>
|
console.log('Mail Server Data:', data); // برای debug
|
||||||
<div className='flex xl:flex-row gap-4 flex-col justify-between xl:items-end border-b pb-10 broder-border'>
|
|
||||||
<div className='flex-1'>
|
if (isLoading) {
|
||||||
<div className='xl:text-lg'>
|
return (
|
||||||
{t('setting.setting_fetch')}
|
<div className='bg-white rounded-4xl p-8 mt-9'>
|
||||||
</div>
|
<div className='text-center py-8'>
|
||||||
<div className='xl:text-sm text-xs text-description mt-1.5'>
|
در حال بارگذاری...
|
||||||
{t('setting.setting_fetch_description')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<RadioGroup
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
label: '۱ دقیقه',
|
|
||||||
value: '1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '۲ دقیقه',
|
|
||||||
value: '2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '۳ دقیقه',
|
|
||||||
value: '3'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
onChange={() => null}
|
|
||||||
selected='1'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className='bg-white rounded-4xl p-8 mt-9'>
|
||||||
|
<div className='text-center py-8 text-red-500'>
|
||||||
|
خطا در بارگذاری اطلاعات
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-white rounded-4xl p-8 mt-9'>
|
||||||
|
{/* SMTP Settings */}
|
||||||
<div className='mt-9'>
|
<div className='mt-9'>
|
||||||
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
|
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<div className='xl:text-lg'>
|
<div className='xl:text-lg'>
|
||||||
{t('setting.setting_smtp')}
|
{t('setting.setting_smtp')}
|
||||||
@@ -53,20 +46,57 @@ const Domain: FC = () => {
|
|||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.smtp_server')}
|
label={t('setting.smtp_server')}
|
||||||
|
value={data?.data?.setupInfo?.smtpSettings?.server || ''}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.smtp_port')}
|
label={t('setting.smtp_port')}
|
||||||
|
value={data?.data?.setupInfo?.smtpSettings?.port?.toString() || ''}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.username')}
|
label={t('setting.encryption')}
|
||||||
|
value={data?.data?.setupInfo?.smtpSettings?.encryption || ''}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IMAP Settings */}
|
||||||
|
<div className='mt-9 border-t pt-9'>
|
||||||
|
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='xl:text-lg'>
|
||||||
|
{t('setting.setting_imap')}
|
||||||
|
</div>
|
||||||
|
<div className='xl:text-sm text-xs text-description mt-1.5'>
|
||||||
|
{t('setting.setting_imap_description')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Input
|
||||||
|
label={t('setting.imap_server')}
|
||||||
|
value={data?.data?.setupInfo?.imapSettings?.server || ''}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Input
|
||||||
|
label={t('setting.imap_port')}
|
||||||
|
value={data?.data?.setupInfo?.imapSettings?.port?.toString() || ''}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-8'>
|
<div className='mt-8'>
|
||||||
<Input
|
<Input
|
||||||
label={t('setting.password')}
|
label={t('setting.encryption')}
|
||||||
|
value={data?.data?.setupInfo?.imapSettings?.encryption || ''}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,4 +106,4 @@ const Domain: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Domain
|
export default MailServer
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getServerInfo } from "../service/Service";
|
||||||
|
|
||||||
|
export const useMailServerData = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["mail-server-data"],
|
||||||
|
queryFn: getServerInfo,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
|
||||||
|
export const getServerInfo = async () => {
|
||||||
|
const { data } = await axios.get("/mail-server/info");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user