This commit is contained in:
hamid zarghami
2025-07-13 13:19:06 +03:30
parent 0cf98ee67c
commit 257b287dfd
13 changed files with 491 additions and 114 deletions
+2
View File
@@ -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'
+8 -5
View File
@@ -1,13 +1,16 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Dmail Admin | داناک</title>
</head> </head>
<body>
<body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
+3 -1
View File
@@ -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
View File
@@ -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;
} }
} }
} }
+69
View File
@@ -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
+13
View File
@@ -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} مطمئن هستید؟"
} }
} }
+216 -42
View File
@@ -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)) {
return
}
if (isEditMode && selectedAddress) {
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) => { onSuccess: (data) => {
toast(data?.data?.message, 'success') toast(data?.data?.message, 'success')
reset()
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error') 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 className='flex justify-between items-center'>
<div> <div>
{t('setting.add_address')} {isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
</div> </div>
{isEditMode && (
{/* <div className='mt-8 flex items-center justify-between'> <Button
<div> variant='secondary'
{t('setting.status')} onClick={handleCancelEdit}
className='px-4 w-fit text-xs'
>
انصراف
</Button>
)}
</div> </div>
<div className='dltr pt-2'>
<Switch />
</div>
</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,13 +334,30 @@ 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>
{!isEditMode && (
<div className='mt-2 text-xs'> <div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div> <div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'> <ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
@@ -203,22 +367,32 @@ const Address: FC = () => {
<li>شامل کاراکتر های خاص (نماد ها) باشد</li> <li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul> </ul>
</div> </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"] });
},
}); });
}; };
+19 -2
View File
@@ -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;
};
+21
View File
@@ -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;
};
};
}
+62 -32
View File
@@ -1,44 +1,37 @@
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()
const { data, isLoading, error } = useMailServerData();
console.log('Mail Server Data:', data); // برای debug
if (isLoading) {
return ( return (
<div className='bg-white rounded-4xl p-8 mt-9'> <div className='bg-white rounded-4xl p-8 mt-9'>
<div> <div className='text-center py-8'>
<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'>
<div className='xl:text-lg'>
{t('setting.setting_fetch')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.setting_fetch_description')}
</div> </div>
</div> </div>
<div className='flex-1'> );
<RadioGroup
items={[
{
label: '۱ دقیقه',
value: '1'
},
{
label: '۲ دقیقه',
value: '2'
},
{
label: '۳ دقیقه',
value: '3'
} }
]}
onChange={() => null} if (error) {
selected='1' return (
/> <div className='bg-white rounded-4xl p-8 mt-9'>
</div> <div className='text-center py-8 text-red-500'>
خطا در بارگذاری اطلاعات
</div> </div>
</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'>
@@ -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;
};